ebox/ebox-incremental.el
Kinneyzhang f1f91468aa
Some checks are pending
CI / test (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
chore: freeze verified ebox baseline before C1b
2026-09-05 05:07:37 +08:00

11047 lines
520 KiB
EmacsLisp

;;; ebox-incremental.el --- Incremental update planner for Ebox -*- lexical-binding: t; -*-
;;; Commentary:
;; Owns dirty-set construction, patch-set planning, viewport invalidation, and
;; update reports. It does not own DSL parsing or low-level buffer spans.
;;; Code:
(require 'cl-lib)
(require 'ebox-cache)
(require 'ebox-fragment)
(require 'ebox-flex)
(require 'ebox-tree)
(require 'ebox-buffer-backend)
(require 'ebox-render-context)
(require 'ebox-patch-plan)
(defvar ebox-region-types)
(declare-function ebox--scroll-state-region-ids-containing-node-ids
"ebox" (buffer node-ids))
(declare-function ebox--scroll-cache-side-effects-for-region-ids
"ebox" (region-id-set))
(declare-function ebox--scroll-cache-actions-replayable-p
"ebox" (actions &optional portable))
(declare-function ebox--scroll-cache-actions-portable-p
"ebox" (actions))
(declare-function ebox--replay-scroll-cache-actions
"ebox" (actions &optional portable))
(declare-function ebox-tree-validate-declarative-root
"ebox-tree"
(root &optional validate-participation-p root-parent-kind))
(declare-function ebox-tree-validate-indexed-participation
"ebox-tree"
(node-table parent-table node-ids
&optional source source-index))
(declare-function ebox-tree-clear-runtime-identities
"ebox-tree" (root))
(declare-function ebox-tree-copy-node-structure
"ebox-tree" (root))
(declare-function ebox-tree-reconcile-runtime
"ebox-tree"
(old old-source-index new new-source-index))
(declare-function ebox-tree-transfer-runtime-identity
"ebox-tree" (old new))
(declare-function ebox-tree-runtime-identity-snapshot
"ebox-tree" (root))
(declare-function tp-object-mounts
"tp-surface" (object))
(declare-function ebox-string-lines "ebox" (string))
(declare-function ebox-get "ebox" (box key))
(declare-function ebox--ensure-node-id "ebox" (node))
(declare-function ebox--ensure-region-id "ebox" (box))
(declare-function ebox--runtime-node-ids "ebox" (node))
(declare-function ebox--schedule-buffer-runtime-prewarm
"ebox" (buffer))
(declare-function ebox--cancel-buffer-runtime-prewarm
"ebox" (buffer))
(declare-function ebox--scroll-clear-state
"ebox" (region-id))
(declare-function ebox--scroll-cancel-idle-prefetch
"ebox" (region-id))
(declare-function ebox--scroll-schedule-idle-prefetch
"ebox" (region-id &optional delay))
(declare-function ebox--scroll-window-rebind-state-producers
"ebox-layout" (state box))
(declare-function ebox--smooth-scroll-stop
"ebox" (region-id))
(declare-function ebox--node-children
"ebox" (node))
(declare-function ebox--root-region-box
"ebox" (root region-id))
(declare-function ebox--literal-root-pixel-width
"ebox" (box))
(declare-function ebox--format-content
"ebox-layout" (box))
(declare-function ebox-put "ebox" (box key value))
(declare-function ebox--confirm-reflow-prewarm-scratch
"ebox" (buffer region-id revision))
(declare-function ebox-cache-publish-report
"ebox-cache" (buffer report))
(declare-function ebox-cache-snapshot-report
"ebox-cache" (buffer))
(declare-function ebox-cache-restore-report
"ebox-cache" (buffer snapshot))
(declare-function ebox-style-cascade-active-p
"ebox-style" ())
(declare-function ebox-native-commit-plan-eligible-p
"ebox-native-commit"
(old-state candidate-state prepared))
(declare-function ebox-native-commit-prepare-viewport-continuity
"ebox-native-commit"
(previous-state candidate-state))
(declare-function ebox-native-commit-topology-stable-p
"ebox-native-commit" (old-state candidate-state prepared))
(declare-function ebox-native-reflow-release-session
"ebox-native-reflow" (session))
(cl-defstruct
(ebox-incremental-context-port
(:constructor ebox-incremental-context-port-create))
"Read-only host queries supplied by the Ebox orchestrator."
(buffer-state nil :read-only t)
(region-mounts nil :read-only t)
(cascade-owner-proof-p nil :read-only t)
(prepare-inline-styles nil :read-only t))
(defvar ebox-incremental--context-port nil
"Immutable host-query port installed by the Ebox orchestrator.")
(defun ebox-incremental-install-context-port (port)
"Install validated immutable incremental context PORT."
(unless (and (ebox-incremental-context-port-p port)
(cl-every
#'functionp
(list
(ebox-incremental-context-port-buffer-state port)
(ebox-incremental-context-port-region-mounts port)
(ebox-incremental-context-port-cascade-owner-proof-p port)
(ebox-incremental-context-port-prepare-inline-styles port))))
(signal 'wrong-type-argument
(list 'ebox-incremental-context-port-p port)))
(setq ebox-incremental--context-port port))
(defun ebox-incremental--require-context-port ()
"Return the installed incremental context port or fail closed."
(or ebox-incremental--context-port
(error "Ebox incremental context port is not installed")))
(defun ebox-incremental--buffer-state (buffer)
"Return BUFFER's live client state through the context port."
(funcall
(ebox-incremental-context-port-buffer-state
(ebox-incremental--require-context-port))
buffer))
(defun ebox-incremental--region-mounts (buffer region-id &optional roles)
"Return BUFFER mounts for REGION-ID and optional ROLES through the port."
(funcall
(ebox-incremental-context-port-region-mounts
(ebox-incremental--require-context-port))
buffer region-id roles))
(defun ebox-incremental--cascade-local-owner-proof-p
(state candidate-state dirty-set)
"Return the port proof for STATE, CANDIDATE-STATE, and DIRTY-SET."
(funcall
(ebox-incremental-context-port-cascade-owner-proof-p
(ebox-incremental--require-context-port))
state candidate-state dirty-set))
(defun ebox-incremental--prepare-inline-candidate-styles
(old-state candidate-index node-ids)
"Prepare NODE-IDS in CANDIDATE-INDEX using OLD-STATE through the port."
(funcall
(ebox-incremental-context-port-prepare-inline-styles
(ebox-incremental--require-context-port))
old-state candidate-index node-ids))
(defvar ebox-viewport-width)
(defvar ebox-viewport-height)
(defvar ebox--region-id-counter)
(defvar ebox--runtime-node-id-counter)
(defvar ebox--region-box-table)
(defvar ebox--scroll-global-state)
(defvar ebox--scroll-idle-prefetch-timers)
(defvar ebox--smooth-scroll-state-table)
(defvar ebox--render-region-id)
(defvar ebox--defer-scroll-content-index)
(defvar ebox--flex-content-min-width-table)
(defvar ebox--layout-fragments-table)
(defvar ebox--layout-fragments-reuse-p)
;;; Render Cache
(defvar ebox--render-cache-table nil
"Dynamic buffer-local render cache used during incremental rerenders.")
(defvar ebox--prepared-root-render nil
"Dynamically bound validated root render for one visible transaction.")
(defvar ebox--collect-rebuilt-scroll-state-region-ids nil
"Non-nil while one update records scroll states rebuilt by rendering.")
(defvar ebox--rebuilt-scroll-state-region-ids nil
"Scroll region ids rebuilt in the current recorded render transaction.")
(defvar ebox--render-cache-scroll-state-region-ids nil
"Dynamically bound set of scroll region ids touched by one cached render.")
(defvar ebox--render-cache-scroll-state-restorable-p t
"Non-nil when cached scroll state belongs to the live runtime tree.
Isolated prewarming binds this to nil because its prefix renderers capture a
copied tree and must never be published into the live buffer runtime.")
(defvar ebox--render-cache-scroll-state-retained-cost-cache nil
"Dynamic identity cache for scroll snapshot costs in one root render.")
(defvar ebox-incremental-before-runtime-mutation-hook nil
"Functions run before an existing buffer runtime is mutated.
Each function receives the target buffer and a mutation kind. Hooks may
invalidate speculative work, but must not mutate the pending operation's
arguments.")
(defvar ebox-incremental--runtime-mutation-hooks-inhibited-p nil
"Non-nil while a validated prepared-root transaction owns its mutation.")
(defvar ebox-incremental--declarative-commit-buffer nil
"Buffer whose declarative root commit currently owns publication.")
(defvar ebox-incremental--source-base-index nil
"Transaction-local source facts transported by one canonical input.")
(defvar ebox-incremental--previous-source-base-index nil
"Transaction-local previous source facts for one paired paint proof.")
(defvar ebox-incremental--candidate-incoming-source-indexes nil
"Transaction-local handle to input-index map for one logical candidate.")
(defvar ebox-incremental--candidate-full-source-base-index nil
"Transaction-local fact index for one broad candidate rebuild.")
(defun ebox-incremental--candidate-range-items-retainable-p
(candidate items source-index reuse-map)
"Return non-nil when ITEMS satisfy the candidate-owned retention proof.
The proof is deliberately narrow: every non-reused node must belong to the
incoming canonical source generation and carry no Ebox runtime identity.
Published reuse slots are skipped because they are replaced by their exact old
objects during canonical reuse. Canonical input construction has already
checked node/handle uniqueness; this check adds the ownership boundary against
the published candidate base without scanning that base's full tree."
(let* ((base-state (ebox-candidate--base-state candidate))
(base-source-index (plist-get base-state :source-index))
(seen (make-hash-table :test #'eq)))
(and (ebox-source-index-p source-index)
(not (eq source-index base-source-index))
(cl-labels
((visit
(node)
(and (listp node)
(not (stringp node))
(not (ebox-child-range--descriptor-p node))
(not (gethash node seen))
(let ((handle (ebox-tree-node-source-handle node)))
(and (ebox-source-index-handle-member-p
source-index handle)
(not (and base-source-index
(ebox-source-index-handle-member-p
base-source-index handle)))
(null (plist-get node :node-id))
(null (plist-get node :region-id))
(null (plist-get node :surface-object))
(null (plist-get node :ebox-sequence-location))
(progn
(puthash node t seen)
(cl-every #'visit
(ebox-tree--children-raw node))))))))
(cl-loop for item in items for index from 0
unless (assq index reuse-map)
always (visit item))))))
(cl-defstruct
(ebox-candidate
(:constructor ebox-incremental--make-candidate)
(:conc-name ebox-candidate--))
"One-shot logical declarative transaction based on a published runtime."
buffer
base-state
base-root
base-revision
base-buffer-tick
replacements
paint-patches
range-replacements
range-metrics
sealed-p)
(cl-defstruct
(ebox-incremental--candidate-replacement
(:constructor ebox-incremental--make-candidate-replacement)
(:conc-name ebox-incremental--candidate-replacement-))
"One logical anchor replacement and its optional semantic transition."
anchor-id
subtree
source-index
old-semantic-key
new-semantic-key)
(cl-defstruct
(ebox-incremental--candidate-range-replacement
(:constructor ebox-incremental--make-candidate-range-replacement)
(:conc-name ebox-incremental--candidate-range-replacement-))
"One base-addressed child Range payload replacement."
ref items reuse-map source-indexes retain-item-identities-p)
(cl-defstruct
(ebox-incremental--candidate-paint-patch
(:constructor ebox-incremental--make-candidate-paint-patch)
(:conc-name ebox-incremental--candidate-paint-patch-))
"One base-addressed Host paint update without subtree replacement."
anchor-id node source-index changed-keys)
(cl-defstruct
(ebox-incremental--detached-history
(:constructor ebox-incremental--make-detached-history)
(:conc-name ebox-incremental--detached-history-))
"One buffer-owned bounded detached runtime identity history."
table
order
node-count)
(defvar ebox-incremental--candidate-proof-node-table nil
"Dynamic `(BUFFER . TABLE)' for proof-local owner subtree copies.")
(defvar ebox-incremental--candidate-base-state nil
"Dynamic `(BUFFER . STATE)' holding proof-time published runtime nodes.")
(defun ebox-incremental--proof-base-state (buffer)
"Return BUFFER's published state while candidate lookups are overridden."
(or (and ebox-incremental--candidate-base-state
(eq buffer (car ebox-incremental--candidate-base-state))
(cdr ebox-incremental--candidate-base-state))
(ebox--buffer-render-state buffer)))
(defvar ebox-incremental--candidate-path-copy-trace nil
"Dynamic hash table receiving logical candidate path-copy facts.
Each key is a base anchor node id. Values describe the final copied node,
its candidate parent, whether it is a replacement anchor, and whether a
direct child runtime id changed. The trace lets index preparation skip every
untouched sibling without exposing paths outside Ebox.")
(defvar ebox-incremental--candidate-path-copy-origin-table nil
"Dynamic current-node-id to base-origin-id map for candidate path copies.
Type-changing ancestor replacements receive fresh runtime ids. Later
descendant refinements still use those fresh ids while copying their path, so
this transaction-local map keeps every successive copy recorded under the
original published identity. Candidate-only nodes with no base origin map to
themselves.")
(defvar ebox-incremental--candidate-range-index-deltas nil
"Transaction-local Range parent and affected payload index deltas.")
(defvar ebox-incremental--candidate-state-for-slot-proof nil
"Candidate runtime used by bounded allocated-slot impact proofs.")
(defvar ebox-incremental--allocated-slot-proof-cache nil
"Transaction-local cache of candidate allocated-slot proof results.")
(defvar ebox-incremental--after-declarative-publication nil
"Internal callback run inside declarative publication's quit-free boundary.
Framework adapters may dynamically bind this to a one-argument, pointer-only
promotion function receiving the candidate update report. It must not call
user code or signal after mutation.")
(defun ebox-incremental--notify-before-runtime-mutation (buffer kind)
"Notify speculative consumers before BUFFER runtime mutation KIND."
(unless ebox-incremental--runtime-mutation-hooks-inhibited-p
(run-hook-with-args
'ebox-incremental-before-runtime-mutation-hook buffer kind)))
(defvar ebox--buffer-render-state-table)
(declare-function tp-surface-client-state "tp-surface" (surface))
(declare-function tp-surface-live-p "tp-surface" (surface))
(defvar ebox-render-cache-max-entries 2048
"Maximum persistent render cache entries retained per buffer.
The public customization is declared by the `ebox' facade after internal
modules have loaded.")
(defvar ebox-render-cache-max-bytes (* 32 1024 1024)
"Approximate persistent render cache byte budget per buffer.
The public customization is declared by the `ebox' facade after internal
modules have loaded.")
(defvar ebox-render-root-cache-max-entries 16
"Maximum complete root render outputs retained per buffer.
The public customization is declared by the `ebox' facade after internal
modules have loaded.")
(defvar ebox-render-root-cache-max-bytes (* 8 1024 1024)
"Approximate complete root render output budget per buffer.
The public customization is declared by the `ebox' facade after internal
modules have loaded.")
(defvar ebox-incremental--detached-history-entry-limit 16
"Maximum semantic subtree identities retained per buffer.")
(defvar ebox-incremental--detached-history-node-limit 8192
"Maximum identity skeleton nodes retained per buffer.")
(defvar ebox--render-cache-ring-table
(make-hash-table :test 'eq :weakness 'key)
"Weak map from render cache tables to fixed-capacity eviction rings.")
(defvar ebox--render-root-cache-table-table
(make-hash-table :test 'eq :weakness 'key)
"Weak map from subtree caches to their bounded complete-root caches.")
(defvar ebox--render-cache-entry-side-effects-table
(make-hash-table :test 'eq :weakness 'key)
"Weak cache-entry map for render-time scroll state side effects.")
(defvar ebox--render-cache-signature-cache nil
"Dynamic render-pass cache for recursive render signatures.")
(defvar ebox--scroll-prefetch-delay-override nil)
(defvar ebox--scroll-window-initial-lookahead-lines-override nil)
(defvar ebox--root-reflow-gc-deferred-p nil
"Non-nil while an outer root reflow already defers garbage collection.")
(defvar ebox--prepared-root-defer-index-refresh nil
"Prepared animation index policy.
Nil uses ordinary root refresh, `incremental' splices exact Rust patch spans,
and any other non-nil value defers broad indexes for a display-only frame.")
(defconst ebox--root-reflow-scroll-lookahead-lines 13
"Lazy scroll lines retained by latency-sensitive root reflows.
The scroll window already renders one completion sentinel, so this leaves a
14-line published runway; an immediate exact-prefix extension fills the rest
of a default smooth-wheel step without rebuilding the rendered prefix.")
(defconst ebox--latency-sensitive-scroll-node-limit 512
"Runtime size above which lazy scroll rows stay strictly demand-driven.
Large indexed runtimes can hide a composite flex subtree behind one logical
scroll line. Bounding work by requested lines alone is therefore insufficient
unless root reflow and idle prefetch also avoid crossing several such rows.")
(defun ebox--buffer-latency-sensitive-scroll-prefix-p (buffer)
"Return non-nil when BUFFER's lazy scroll rows need atomic scheduling."
;; A logical candidate may deliberately defer rebuilding selector paths
;; until the first selector query. Scroll policy needs only the retained
;; runtime type membership and must not turn proof rendering into a
;; logical-tree selector-index rebuild.
(let* ((state (ebox--buffer-render-state buffer))
(type-counts (plist-get state :runtime-type-count-table))
(node-table (plist-get state :node-table)))
(or (plist-get state :selector-index-stale-p)
(and (hash-table-p type-counts)
(> (or (gethash 'flex type-counts) 0) 0))
(and (hash-table-p node-table)
(>= (hash-table-count node-table)
ebox--latency-sensitive-scroll-node-limit)))))
(defun ebox--root-reflow-scroll-lookahead-lines-for-buffer (buffer)
"Return the root-reflow lazy scroll runway for BUFFER.
Explicit flex items and large runtimes can render an expensive composite row
for one requested line. Keep those rows demand-driven; other sources retain
the short runway that makes immediate post-resize smooth scrolling cheap."
(if (ebox--buffer-latency-sensitive-scroll-prefix-p buffer)
0
ebox--root-reflow-scroll-lookahead-lines))
(defvar ebox--viewport-dependent-node-ids-cache nil
"Dynamic render-pass cache for viewport dependency discovery.")
(defvar ebox--viewport-dependent-subtree-cache nil
"Dynamic render-pass cache for viewport dependency existence checks.")
(defvar ebox--inline-auto-width-intrinsic-p)
(defvar ebox--viewport-height-dependent-subtree-cache nil
"Dynamic render-pass cache for viewport-height dependency checks.")
(defvar ebox--render-cache-allow-viewport-dependent nil
"Non-nil allows caching viewport-dependent nodes for the current context.")
(defun ebox--render-cache-contained-viewport-node-p (node)
"Return non-nil when NODE bounds descendant viewport-dependent layout."
(and (eq (and (listp node) (plist-get node :ebox-type)) 'box)
(ebox--span-patch-definite-size-value-p (ebox-get node :width))))
(defun ebox--render-cacheable-node-p (node)
"Return non-nil when NODE can reuse rendered output in the current context."
(and ebox--render-cache-table
(listp node)
(not (stringp node))
(plist-get node :ebox-type)
(or ebox--render-cache-allow-viewport-dependent
(not (ebox--viewport-dependent-subtree-p node))
(ebox--render-cache-contained-viewport-node-p node))))
(defun ebox--render-cacheable-box-p (node)
"Return non-nil when NODE is a cacheable box.
Kept as a narrow predicate for callers that need box-only sizing semantics."
(and (eq (and (listp node) (plist-get node :ebox-type)) 'box)
(ebox--render-cacheable-node-p node)))
(defun ebox--render-cache-value-signature (value)
"Return VALUE normalized for render cache signatures."
(cond
((ebox-child-range--sequence-p value)
(cons 'ebox-child-sequence
(mapcar #'ebox--render-cache-value-signature
(ebox-child-range--flatten value))))
((and (listp value)
(not (stringp value))
(plist-get value :ebox-type))
(ebox--render-cache-node-body-signature value))
((consp value)
(cons (ebox--render-cache-value-signature (car value))
(ebox--render-cache-value-signature (cdr value))))
(t value)))
(defun ebox--render-cache-plist-signature (node)
"Return NODE's render-relevant plist signature."
(let ((plist node)
signature)
(while plist
(let ((key (pop plist))
(value (pop plist)))
;; These fields are deterministic products of rendering the same
;; canonical node. Including them makes the first render populate a
;; cache key that becomes unreachable as soon as that render records
;; its own proof, forcing every fixed descendant through layout again
;; on the first viewport update.
(unless (memq key '(:render-cache :surface-object
:ebox-sequence-location
:ebox-content-width-exact-p
:ebox-content-layout-complete-p))
(push key signature)
(push (ebox--render-cache-value-signature value) signature))))
(nreverse signature)))
(defun ebox--render-cache-node-body-signature (node)
"Return NODE's recursive body signature, memoized within a render pass."
(if (not (and ebox--render-cache-signature-cache
(listp node)
(not (stringp node))))
(ebox--render-cache-plist-signature node)
(let* ((missing (make-symbol "ebox-render-signature-missing"))
(cached (gethash node ebox--render-cache-signature-cache
missing)))
(if (not (eq cached missing))
cached
(puthash node
(ebox--render-cache-plist-signature node)
ebox--render-cache-signature-cache)))))
(defun ebox--render-cache-node-signature (node)
"Return a cache signature for NODE render output."
;; Cache signatures include rendered region identity because cached text
;; properties carry that identity. Materialize both identity domains
;; before memoizing the body signature; otherwise the first cache probe can
;; run before `ebox--render-box' assigns :region-id and every later probe
;; misses solely because the same node has since been rendered.
(when (listp node)
(ebox--runtime-node-ids node)
(ebox--node-all-region-ids node))
(list (ebox--current-display-signature)
(when (and (ebox--viewport-dependent-subtree-p node)
(not (ebox--render-cache-contained-viewport-node-p
node)))
ebox-viewport-width)
(when (ebox--viewport-height-dependent-subtree-p node)
ebox-viewport-height)
ebox--intrinsic-layout-measurement
ebox--inline-auto-width-intrinsic-p
(ebox--render-cache-node-body-signature node)))
(defun ebox--render-cache-box-signature (box)
"Return a cache signature for BOX render output."
(ebox--render-cache-node-signature box))
(defsubst ebox--render-cache-effective-key (cache-key signature)
"Return CACHE-KEY namespaced by the complete render SIGNATURE.
The complete signature keeps distinct values in one `sxhash-equal' bucket."
(list cache-key signature))
(defun ebox--render-cache-retained-cost (object &optional limit)
"Return a conservative retained-byte estimate for OBJECT up to LIMIT.
Rendered Ebox strings encode most geometry in text-property intervals rather
than character bytes. Charging every character as one possible interval keeps
the estimate cheap enough for cache misses while bounding those hidden values.
When LIMIT is non-nil, stop once the estimate exceeds it; callers only need to
know that an oversized value cannot be retained."
(let ((seen (make-hash-table :test 'eq))
(stack (list object))
(total 0))
(while (and stack
(or (null limit) (<= total limit)))
(let ((current (pop stack)))
(unless (or (null current)
(numberp current)
(symbolp current)
(gethash current seen))
(puthash current t seen)
(cond
((stringp current)
(cl-incf total (+ 64 (string-bytes current)
(* 128 (length current)))))
((consp current)
(cl-incf total 16)
(push (car current) stack)
(push (cdr current) stack))
((vectorp current)
(cl-incf total (+ 32 (* 8 (length current))))
(dotimes (index (length current))
(push (aref current index) stack)))
((hash-table-p current)
(cl-incf total 64)
(maphash (lambda (key value)
(push key stack)
(push value stack))
current))))))
total))
(defun ebox--render-cache-side-effect-string-cost (string)
"Return retained bytes for one scroll metadata STRING.
The snapshot owns line strings separately from the cached output. Charge a
bounded property-bearing character estimate without walking each interval;
doing so here would put cache accounting itself on the resize hot path."
(+ 64 (string-bytes string) (* 16 (length string))))
(defun ebox--render-cache-side-effects-retained-cost
(metadata &optional limit)
"Return bounded retained bytes for scroll side-effect METADATA.
Live boxes are already owned by the runtime and renderer closures capture that
same tree, so this charges them shallowly. Snapshot line strings and their
text-property interval spines are charged exactly once by identity."
(let ((seen (make-hash-table :test 'eq))
(total 64))
(cl-labels
((room-p () (or (null limit) (<= total limit)))
(charge-lines (lines)
(while (and lines (room-p))
(cl-incf total 16)
(let ((line (pop lines)))
(when (and (stringp line) (not (gethash line seen)))
(puthash line t seen)
(cl-incf total
(ebox--render-cache-side-effect-string-cost
line)))))))
(dolist (action (plist-get metadata :scroll-actions))
(when (room-p)
(cl-incf total 48)
(when (eq (car action) 'set)
(let ((template (nth 2 action)))
(cl-incf total (* 16 (length template)))
(charge-lines (plist-get template :content-lines))
(charge-lines (plist-get template :rendered-content-lines))
(when (plist-get template :render-content-prefix)
(cl-incf total 512))
(when (plist-get template :materialize-content-lines)
(cl-incf total 256)))))))
total))
(defun ebox--render-cache-entry-retained-cost (entry &optional limit)
"Return retained bytes for ENTRY's value and metadata, up to LIMIT."
(let* ((rendered-cost
(ebox--render-cache-retained-cost (cdr entry) limit))
(metadata
(gethash entry ebox--render-cache-entry-side-effects-table)))
(if (or (null metadata)
(and limit (> rendered-cost limit)))
rendered-cost
(+ rendered-cost
(or (plist-get metadata :retained-cost)
(ebox--render-cache-side-effects-retained-cost
metadata (and limit (max 0 (- limit rendered-cost)))))))))
(defsubst ebox--render-cache-entry-limit ()
"Return the positive configured render cache entry limit."
(max 1 ebox-render-cache-max-entries))
(defsubst ebox--render-cache-byte-limit ()
"Return the positive configured render cache byte limit."
(max 1 ebox-render-cache-max-bytes))
(defsubst ebox--render-cache-ring-reset (ring entry-limit byte-limit)
"Reset RING metadata for ENTRY-LIMIT and BYTE-LIMIT."
(aset ring 0 (make-vector entry-limit nil))
(aset ring 1 (make-vector entry-limit 0))
(aset ring 2 0)
(aset ring 3 0)
(aset ring 4 entry-limit)
(aset ring 5 0)
(aset ring 6 byte-limit))
(defsubst ebox--render-cache-ring-evict-oldest (ring)
"Evict and forget RING's oldest entry."
(when (> (aref ring 3) 0)
(let* ((keys (aref ring 0))
(costs (aref ring 1))
(start (aref ring 2))
(cost (aref costs start))
(effective-key (aref keys start))
(entry (gethash effective-key ebox--render-cache-table)))
(remhash effective-key ebox--render-cache-table)
(when entry
(remhash entry ebox--render-cache-entry-side-effects-table))
(aset keys start nil)
(aset costs start 0)
(aset ring 2 (% (1+ start) (aref ring 4)))
(aset ring 3 (1- (aref ring 3)))
(aset ring 5 (max 0 (- (aref ring 5) cost))))))
(defsubst ebox--render-cache-ring-enforce-byte-limit (ring)
"Evict oldest RING entries until its byte budget is satisfied."
(while (and (> (aref ring 3) 0)
(> (aref ring 5) (aref ring 6)))
(ebox--render-cache-ring-evict-oldest ring)))
(defsubst ebox--render-cache-ring-append (ring effective-key cost)
"Append EFFECTIVE-KEY with retained COST to cache eviction RING."
(when (= (aref ring 3) (aref ring 4))
(ebox--render-cache-ring-evict-oldest ring))
(let* ((keys (aref ring 0))
(costs (aref ring 1))
(index (% (+ (aref ring 2) (aref ring 3)) (aref ring 4))))
(aset keys index effective-key)
(aset costs index cost)
(aset ring 3 (1+ (aref ring 3)))
(aset ring 5 (+ (aref ring 5) cost)))
(ebox--render-cache-ring-enforce-byte-limit ring))
(defun ebox--render-cache-ring ()
"Return eviction ring owned by the current render cache table."
(or (gethash ebox--render-cache-table ebox--render-cache-ring-table)
(let* ((entry-limit (ebox--render-cache-entry-limit))
(byte-limit (ebox--render-cache-byte-limit))
(ring (vector (make-vector entry-limit nil)
(make-vector entry-limit 0)
0 0 entry-limit 0 byte-limit)))
(puthash ebox--render-cache-table ring ebox--render-cache-ring-table)
ring)))
(defun ebox--render-cache-ring-rebuild (ring entry-limit byte-limit)
"Rebuild RING from the current cache with ENTRY-LIMIT and BYTE-LIMIT.
This is reserved for exceptional private mutations that bypass cache stores."
(let (entries)
(maphash (lambda (key entry)
(push (cons key
(ebox--render-cache-entry-retained-cost
entry byte-limit))
entries))
ebox--render-cache-table)
(ebox--render-cache-ring-reset ring entry-limit byte-limit)
(dolist (entry (nreverse entries))
(when (gethash (car entry) ebox--render-cache-table)
(ebox--render-cache-ring-append ring (car entry) (cdr entry))))))
(defun ebox--render-cache-ring-resize (ring entry-limit byte-limit)
"Resize RING while retaining its newest entries within both limits."
(let ((old-keys (aref ring 0))
(old-costs (aref ring 1))
(old-start (aref ring 2))
(old-count (aref ring 3))
(old-limit (aref ring 4))
entries)
(dotimes (offset old-count)
(let ((index (% (+ old-start offset) old-limit)))
(push (cons (aref old-keys index) (aref old-costs index)) entries)))
(ebox--render-cache-ring-reset ring entry-limit byte-limit)
(dolist (entry (nreverse entries))
(when (gethash (car entry) ebox--render-cache-table)
(ebox--render-cache-ring-append ring (car entry) (cdr entry))))))
(defun ebox--render-cache-ring-update-cost (ring effective-key cost)
"Update EFFECTIVE-KEY retained COST in RING, rebuilding if it is absent."
(let ((keys (aref ring 0))
(costs (aref ring 1))
(start (aref ring 2))
(count (aref ring 3))
(limit (aref ring 4))
found)
(dotimes (offset count)
(let ((index (% (+ start offset) limit)))
(when (equal (aref keys index) effective-key)
(aset ring 5 (+ (aref ring 5) (- cost (aref costs index))))
(aset costs index cost)
(setq found t))))
(if found
(ebox--render-cache-ring-enforce-byte-limit ring)
(ebox--render-cache-ring-rebuild
ring (ebox--render-cache-entry-limit) (ebox--render-cache-byte-limit)))))
(defun ebox--render-cache-ring-sync (ring)
"Synchronize RING with the current render cache table.
This normally does no work. It rebuilds metadata only after a defensive
`clrhash' or another exceptional private cache mutation."
(let ((entry-limit (ebox--render-cache-entry-limit))
(byte-limit (ebox--render-cache-byte-limit)))
(cond
((/= (aref ring 3) (hash-table-count ebox--render-cache-table))
(ebox--render-cache-ring-rebuild ring entry-limit byte-limit))
((or (/= (aref ring 4) entry-limit)
(/= (aref ring 6) byte-limit))
(ebox--render-cache-ring-resize ring entry-limit byte-limit)))))
(defun ebox--render-cache-lookup-entry (cache-key signature)
"Return the raw cache entry for CACHE-KEY matching SIGNATURE."
(let ((entry (gethash (ebox--render-cache-effective-key
cache-key signature)
ebox--render-cache-table)))
(and entry
(equal-including-properties (car entry) signature)
entry)))
(defun ebox--render-cache-lookup (cache-key signature)
"Return cached render output for CACHE-KEY matching SIGNATURE."
(let ((entry (ebox--render-cache-lookup-entry cache-key signature)))
(if (and entry
(equal-including-properties (car entry) signature))
(prog1 (cdr entry)
(when ebox-cache-report-buffer
(ebox-cache-record-hit ebox-cache-report-buffer 'render-body)))
(when ebox-cache-report-buffer
(ebox-cache-record-miss ebox-cache-report-buffer 'render-body))
nil)))
(defun ebox--render-cache-store
(cache-key signature rendered &optional side-effects)
"Store RENDERED for CACHE-KEY and SIGNATURE with SIDE-EFFECTS metadata."
(let* ((effective-key
(ebox--render-cache-effective-key cache-key signature))
(old-entry (gethash effective-key ebox--render-cache-table))
(new-entry-p
(null old-entry))
(ring (ebox--render-cache-ring))
(entry (cons signature rendered)))
(ebox--render-cache-ring-sync ring)
(when old-entry
(remhash old-entry ebox--render-cache-entry-side-effects-table))
(puthash effective-key entry ebox--render-cache-table)
(when side-effects
(puthash entry side-effects ebox--render-cache-entry-side-effects-table))
(let ((cost (ebox--render-cache-entry-retained-cost entry (aref ring 6))))
(cond
((> cost (aref ring 6))
;; The budget contract: an oversized value stays renderable for
;; this caller but is never retained. Appending it first would
;; make byte enforcement evict every healthy older entry before
;; discarding the oversized entry itself.
(remhash effective-key ebox--render-cache-table)
(remhash entry ebox--render-cache-entry-side-effects-table)
(unless new-entry-p
(ebox--render-cache-ring-rebuild
ring (ebox--render-cache-entry-limit)
(ebox--render-cache-byte-limit))))
(new-entry-p
(ebox--render-cache-ring-append ring effective-key cost))
(t
(ebox--render-cache-ring-update-cost ring effective-key cost)))))
rendered)
(defun ebox--render-cache-scroll-actions-stateful-p (actions)
"Return non-nil when ACTIONS contain a scroll state installation."
(cl-some (lambda (action) (eq (car action) 'set)) actions))
(defun ebox--render-cache-entry-reusable-p (entry)
"Return non-nil when ENTRY can safely replay in the current render tree."
(let ((metadata
(gethash entry ebox--render-cache-entry-side-effects-table)))
(or (null metadata)
(not (plist-get metadata :stateful-p))
(and (plist-get metadata :restorable-p)
(or ebox--render-cache-scroll-state-restorable-p
(plist-get metadata :portable-p))
(ebox--scroll-cache-actions-replayable-p
(plist-get metadata :scroll-actions)
(plist-get metadata :portable-p))))))
(defun ebox--render-cache-replay-entry-side-effects (entry)
"Replay ENTRY's scroll side effects before its rendered output is reused."
(when-let* ((metadata
(gethash entry ebox--render-cache-entry-side-effects-table)))
(ebox--replay-scroll-cache-actions
(plist-get metadata :scroll-actions)
(plist-get metadata :portable-p))))
(defun ebox--render-cache-merge-scroll-region-ids (source target)
"Merge scroll region ids from SOURCE hash table into TARGET."
(when (and (hash-table-p source) (hash-table-p target))
(maphash (lambda (region-id _)
(puthash region-id t target))
source)))
(defun ebox--render-cache-render-with-scroll-actions (node)
"Render NODE and return its output plus complete scroll side effects."
(let ((parent-region-ids ebox--render-cache-scroll-state-region-ids)
(region-ids (make-hash-table :test 'equal))
(ebox--render-cache-scroll-state-retained-cost-cache
(or ebox--render-cache-scroll-state-retained-cost-cache
(make-hash-table :test 'eq)))
rendered)
(let ((ebox--render-cache-scroll-state-region-ids region-ids))
(setq rendered (ebox--render-node node)))
(ebox--render-cache-merge-scroll-region-ids
region-ids parent-region-ids)
(list rendered
(ebox--scroll-cache-side-effects-for-region-ids region-ids))))
(defun ebox--render-root-cache-table (subtree-cache)
"Return the bounded complete-root cache paired with SUBTREE-CACHE."
(or (gethash subtree-cache ebox--render-root-cache-table-table)
(let ((root-cache (make-hash-table :test 'equal)))
(puthash subtree-cache root-cache ebox--render-root-cache-table-table)
root-cache)))
(defun ebox--prepared-root-render-matches-p (node prepared)
"Return non-nil when PREPARED is exact for NODE's current transaction."
(let* ((state (gethash (current-buffer) ebox--buffer-render-state-table))
(kind (plist-get prepared :kind))
(region-id (plist-get prepared :region-id)))
(and state
(eq state (plist-get prepared :render-state))
(eq node (plist-get state :root-node))
(equal (ebox--ensure-node-id node)
(plist-get prepared :node-id))
(equal (plist-get state :runtime-revision)
(plist-get prepared :runtime-revision))
(equal (ebox--current-display-signature)
(plist-get prepared :display-signature))
(equal (plist-get state :viewport-width)
(plist-get prepared :viewport-width))
(equal (plist-get state :viewport-height)
(plist-get prepared :viewport-height))
(or (stringp (plist-get prepared :rendered))
(plist-get (plist-get prepared :native-patch) :native-patch))
(pcase kind
('viewport t)
('root-width
(when-let* ((box (ebox--root-region-box node region-id)))
(equal (ebox--literal-root-pixel-width box)
(plist-get prepared :root-width))))
(_ nil)))))
(defun ebox--take-prepared-root-render (node)
"Consume and return a validated dynamic root render for NODE."
(when (and ebox--prepared-root-render
(stringp (plist-get ebox--prepared-root-render :rendered)))
(let ((prepared ebox--prepared-root-render))
(setq ebox--prepared-root-render nil)
(when (ebox--prepared-root-render-matches-p node prepared)
(plist-put prepared :consumed-p t)
(plist-get prepared :rendered)))))
(defun ebox--render-cache-context-signature
(node force &optional external-signature)
"Return NODE's render signature for FORCE and EXTERNAL-SIGNATURE."
(let ((signature
(if (not force)
(ebox--render-cache-node-signature node)
(let ((ebox--viewport-dependent-node-ids-cache
(make-hash-table :test 'eq))
(ebox--viewport-dependent-subtree-cache
(make-hash-table :test 'eq))
(ebox--viewport-height-dependent-subtree-cache
(make-hash-table :test 'eq)))
(ebox--render-cache-node-signature node)))))
(if external-signature
(list signature external-signature)
signature)))
(defun ebox--render-cache-context
(node force &optional external-signature)
"Return exact cache context for NODE, FORCE, and EXTERNAL-SIGNATURE."
(when (or (and (or force external-signature) ebox--render-cache-table)
(ebox--render-cacheable-node-p node))
(let* ((subtree-cache ebox--render-cache-table)
(root-cache-p (and force t)))
(list
:cache-key (list (ebox--ensure-node-id node) 'natural)
:signature
(ebox--render-cache-context-signature
node force external-signature)
:cache (if root-cache-p
(ebox--render-root-cache-table subtree-cache)
subtree-cache)
:entry-limit (if root-cache-p
ebox-render-root-cache-max-entries
ebox-render-cache-max-entries)
:byte-limit (if root-cache-p
ebox-render-root-cache-max-bytes
ebox-render-cache-max-bytes)))))
(defun ebox--render-cache-probe
(node &optional force external-signature)
"Return NODE's exact cache context, with `:rendered' when reusable.
FORCE includes the complete viewport context in the cache signature.
EXTERNAL-SIGNATURE may prove a viewport-dependent non-root context."
(when-let* ((context
(ebox--render-cache-context
node force external-signature)))
(let* ((ebox--render-cache-table (plist-get context :cache))
(entry
(ebox--render-cache-lookup-entry
(plist-get context :cache-key)
(plist-get context :signature))))
(if (and entry (ebox--render-cache-entry-reusable-p entry))
(progn
(when ebox-cache-report-buffer
(ebox-cache-record-hit ebox-cache-report-buffer 'render-body))
(ebox--render-cache-replay-entry-side-effects entry)
(ebox--flex-recache-source-boxes node)
(plist-put context :rendered (cdr entry)))
(when ebox-cache-report-buffer
(ebox-cache-record-miss ebox-cache-report-buffer 'render-body))
context))))
(defun ebox--render-cache-store-context
(context rendered &optional metadata)
"Store RENDERED with METADATA in exact cache CONTEXT."
(let ((ebox--render-cache-table (plist-get context :cache))
(ebox-render-cache-max-entries (plist-get context :entry-limit))
(ebox-render-cache-max-bytes (plist-get context :byte-limit)))
(ebox--render-cache-store
(plist-get context :cache-key)
(plist-get context :signature)
rendered metadata)))
(defun ebox--render-with-cache (node &optional force cache-probe)
"Render NODE, reusing stable subtree output inside buffer render contexts.
When FORCE is non-nil, cache NODE even when its output is viewport-dependent;
the complete viewport context remains part of the render signature.
CACHE-PROBE may supply a prior exact miss from an accelerator boundary."
(if-let* ((prepared (and force
(ebox--take-prepared-root-render node))))
(prog1 prepared
(ebox--replay-render-output-provenance prepared)
(ebox--flex-recache-source-boxes node))
(let ((probe (or cache-probe
(ebox--render-cache-probe node force))))
(cond
((null probe)
(ebox--render-node node))
((plist-member probe :rendered)
(ebox--replay-render-output-provenance
(plist-get probe :rendered)))
(t
(pcase-let* ((`(,rendered ,side-effects)
(ebox--render-cache-render-with-scroll-actions node))
(actions (plist-get side-effects :scroll-actions))
(stateful-p
(ebox--render-cache-scroll-actions-stateful-p actions))
(portable-p
(and stateful-p
(not ebox--render-cache-scroll-state-restorable-p)
(ebox--scroll-cache-actions-portable-p actions)))
(metadata
(append
side-effects
(list :stateful-p stateful-p
:portable-p portable-p
:restorable-p
(or ebox--render-cache-scroll-state-restorable-p
portable-p)))))
;; An isolated copy may still warm pure descendant entries, but a
;; cached node that created scroll state owns closures over that
;; copy and cannot safely cross into the live runtime.
(unless (and stateful-p
(not (or ebox--render-cache-scroll-state-restorable-p
portable-p)))
(ebox--render-cache-store-context probe rendered metadata)
(ebox--record-render-output-provenance rendered))
rendered))))))
;;; Buffer Runtime State
(defvar ebox--buffer-render-state-table (make-hash-table :test 'eq)
"Maps buffer objects to render state plists.
Each state contains :root-node, viewport dimensions, and derived layout
snapshots so every incremental rerender can use the same layout context as the
original buffer render.")
(defvar ebox-incremental--buffer-render-state-override nil
"Dynamically bound `(BUFFER . STATE)' used while preparing a candidate.")
(defvar ebox-incremental--batch-table (make-hash-table :test 'eq)
"Buffer-keyed explicit incremental batch state.")
(defvar ebox-incremental--after-successful-batch-flush-hook nil
"Functions called after a non-empty explicit batch flush succeeds.
Each function receives BUFFER, the original PENDING entries, and the final
update REPORT.")
(defvar ebox--scroll-global-state (make-hash-table :test 'equal)
"Global hash table storing scroll state.
Keys are region-id, values are plists with :scroll-offset, :content-lines, etc.")
(defun ebox--buffer-render-state (buffer)
"Return render state for BUFFER."
(if (and ebox-incremental--buffer-render-state-override
(eq buffer
(car ebox-incremental--buffer-render-state-override)))
(cdr ebox-incremental--buffer-render-state-override)
(or (and (buffer-live-p buffer)
(ebox-incremental--buffer-state buffer))
(gethash buffer ebox--buffer-render-state-table))))
(defun ebox--buffer-update-report (buffer)
"Return the update report stored in BUFFER's render state."
(plist-get (ebox--buffer-render-state buffer) :last-update-report))
(defun ebox--set-buffer-update-report (buffer report)
"Store REPORT in BUFFER's render state and return REPORT."
(let ((state (ebox--buffer-render-state buffer)))
(unless state
(error "Ebox cannot store an update report without render state: %S"
buffer))
(plist-put state :last-update-report report)
report))
(defun ebox--buffer-root-node (buffer)
"Return the render root node stored for BUFFER."
(plist-get (ebox--buffer-render-state buffer) :root-node))
(defun ebox--buffer-root-node-id (buffer)
"Return BUFFER's root runtime node id."
(when-let* ((root (ebox--buffer-root-node buffer)))
(ebox--ensure-node-id root)))
(defun ebox--runtime-region-id-conflict (region-id-set target-buffer)
"Return `(REGION-ID . BUFFER)' for REGION-ID-SET outside TARGET-BUFFER."
(ebox--prune-dead-buffer-render-states)
(let ((target (and target-buffer (get-buffer target-buffer))))
(catch 'conflict
(maphash
(lambda (buffer state)
(unless (eq buffer target)
(when-let* ((owner-set (plist-get state :region-id-set)))
(maphash
(lambda (region-id _present)
(when (gethash region-id owner-set)
(throw 'conflict (cons region-id buffer))))
region-id-set))))
ebox--buffer-render-state-table)
nil)))
(defun ebox-incremental--adjust-runtime-type-count (table node delta)
"Adjust NODE's raw selector-type count in TABLE by DELTA."
(when-let* ((type (ebox-tree-node-selector-type node)))
(let ((count (+ (or (gethash type table) 0) delta)))
(cond
((< count 0)
(error "Ebox runtime type count became negative: %S" type))
((= count 0) (remhash type table))
(t (puthash type count table))))))
(defun ebox--runtime-index
(root-node &optional collect-region-boxes source-index)
"Return persistent runtime indexes for ROOT-NODE.
When COLLECT-REGION-BOXES is non-nil, collect `:region-box-table' during the
same traversal in complete-render overwrite order. SOURCE-INDEX, when
supplied, is the candidate-owned source snapshot for ROOT-NODE."
(let ((node-table (make-hash-table :test 'equal))
(parent-table (make-hash-table :test 'equal))
(region-id-set (make-hash-table :test 'equal))
(region-node-table (make-hash-table :test 'equal))
(region-box-table
(and collect-region-boxes (make-hash-table :test 'equal)))
(region-box-count-table (make-hash-table :test 'equal))
(host-ref-table (make-hash-table :test 'equal))
(range-ref-table (make-hash-table :test 'equal))
(runtime-type-count-table (make-hash-table :test 'eq))
native-node-postorder)
(cl-labels ((index-host-ref (node node-id)
(when-let* ((handle (plist-get node :ebox-source-handle))
(host-ref (ebox-source--handle-id-view handle)))
(let ((count (hash-table-count host-ref-table)))
(puthash host-ref node-id host-ref-table)
(when (= count (hash-table-count host-ref-table))
(error
"Ebox declarative tree uses duplicate host ref %S"
host-ref)))))
(index-region-id (node node-id)
(pcase (plist-get node :ebox-type)
('box
(let ((region-id (ebox--ensure-region-id node)))
(puthash region-id t region-id-set)
(puthash region-id
(1+ (or (gethash region-id
region-box-count-table)
0))
region-box-count-table)
;; A flex wrapper is visited after its flex owner. Keep
;; that earlier render-owner mapping instead of
;; replacing it with the non-renderable source box.
(unless (gethash region-id region-node-table)
(puthash region-id node-id region-node-table))))
('flex
(when-let* ((box (plist-get node :box)))
(let ((region-id (ebox--ensure-region-id box)))
(puthash region-id t region-id-set)
;; Preserve the recursive lookup's first-match
;; semantics when malformed trees reuse a region id.
(unless (gethash region-id region-node-table)
(puthash region-id node-id
region-node-table)))))))
(index-region-box (node)
;; Box rendering records region ownership after rendering
;; child content. Preserve that postorder overwrite order
;; so duplicate legacy ids resolve exactly as they do after
;; a complete render, including flex container wrappers.
(when region-box-table
(pcase (plist-get node :ebox-type)
('box
(puthash (ebox--ensure-region-id node)
node region-box-table))
('flex
(when-let* ((box (plist-get node :box)))
(puthash (ebox--ensure-region-id box)
box region-box-table))))))
(visit (node parent-id native-layout-p)
(when (and (listp node)
(not (stringp node)))
(let ((node-id (ebox--ensure-node-id node))
(type (plist-get node :ebox-type)))
(puthash node-id node node-table)
(ebox-incremental--adjust-runtime-type-count
runtime-type-count-table node 1)
(when parent-id
(puthash node-id parent-id parent-table))
(index-region-id node node-id)
(index-host-ref node node-id)
(when-let* ((sequence (plist-get node :ebox-child-sequence)))
(dotimes
(segment-index
(ebox-child-range--sequence-count sequence))
(let ((payload
(ebox-child-range--segment-payload
(ebox-child-range--segment-at
sequence segment-index))))
(dotimes (offset (length payload))
(let ((child (aref payload offset)))
(ebox--ensure-node-id child)
(plist-put child :ebox-sequence-location
(list :parent-node-id node-id
:segment-index segment-index
:offset offset))))))
(dolist (record
(ebox-child-range--range-records sequence))
(pcase-let ((`(,ref ,segment-index) record))
(when (gethash ref range-ref-table)
(error "Ebox child Range ref is not unique: %S" ref))
(puthash ref
(list :parent-node-id node-id
:segment-index segment-index)
range-ref-table))))
(ebox-tree-for-each-direct-child
node
(lambda (child)
(visit
child node-id
(and native-layout-p
(not (and (eq type 'flex)
(eq child
(plist-get node :box))))))))
(index-region-box node)
;; This is the same child-before-parent order used by
;; the recursive Layout IR compiler. Flex wrapper
;; source boxes are indexed for ownership without adding
;; duplicate compiler work.
(when native-layout-p
(push node native-node-postorder))))))
(visit root-node nil t))
(setq source-index (or source-index (ebox-tree-source-index root-node)))
(setq source-index
(ebox-source--with-runtime-hosts source-index host-ref-table))
(let ()
(list :node-table node-table
:parent-table parent-table
:region-id-set region-id-set
:region-node-table region-node-table
:region-box-table region-box-table
:region-box-count-table region-box-count-table
:range-ref-table range-ref-table
:source-index source-index
:selector-id-table
(ebox-source--index-selector-table source-index 'id)
:selector-class-table
(ebox-source--index-selector-table source-index 'class)
:selector-type-table
(ebox-source--index-selector-table source-index 'type)
:runtime-type-count-table runtime-type-count-table
:native-node-postorder
(vconcat (nreverse native-node-postorder))))))
(defun ebox-incremental--ensure-buffer-selector-indexes (buffer)
"Return BUFFER's state after materializing deferred selector indexes."
(let ((state (ebox--buffer-render-state buffer)))
(when (and state (plist-get state :selector-index-stale-p))
(let* ((source-index
(ebox-source--with-runtime-hosts
(ebox-tree-source-index
(plist-get state :root-node) nil nil
(plist-get state :source-index))
(ebox-source--index-host-ref-table-view
(plist-get state :source-index))))
(index
(list :selector-id-table
(ebox-source--index-selector-table source-index 'id)
:selector-class-table
(ebox-source--index-selector-table source-index 'class)
:selector-type-table
(ebox-source--index-selector-table source-index 'type))))
(setq state (plist-put state :source-index source-index))
(dolist (key '(:selector-id-table
:selector-class-table
:selector-type-table))
(setq state (plist-put state key (plist-get index key))))
(setq state (plist-put state :selector-index-stale-p nil))
(puthash buffer state ebox--buffer-render-state-table)))
state))
(defun ebox--new-buffer-render-state (root-node)
"Return fresh buffer-independent runtime state for ROOT-NODE."
(list :root-node root-node
:viewport-width ebox-viewport-width
:viewport-height ebox-viewport-height
:display-signature (ebox--current-display-signature)
:layout-snapshots (make-hash-table :test 'equal)
:layout-snapshots-complete-p nil
:layout-snapshot-detail-generation 0
:runtime-revision 0 :last-update-report nil
:render-cache (make-hash-table :test 'equal)
:layout-fragments (make-hash-table :test 'equal)
:layout-fragments-reuse-p nil
:detached-identity-history
(ebox-incremental--make-detached-history
:table (make-hash-table :test 'equal)
:order nil :node-count 0)
:render-signature-cache (make-hash-table :test 'eq)
:flex-content-min-widths (make-hash-table :test 'eq)
:viewport-height-dependent-subtree-cache
(make-hash-table :test 'eq)))
(defun ebox--render-state-install-index (state index)
"Install runtime INDEX tables into candidate STATE and return STATE."
(dolist (key '(:node-table :parent-table :region-id-set :region-node-table
:region-box-count-table :region-box-table
:range-ref-table :source-index
:selector-id-table :selector-class-table
:selector-type-table :runtime-type-count-table
:native-node-postorder))
(setq state (plist-put state key (plist-get index key))))
state)
(defun ebox--make-buffer-render-state (root-node)
"Return unpublished complete runtime state for ROOT-NODE."
(ebox--render-state-install-index
(ebox--new-buffer-render-state root-node)
(ebox--runtime-index root-node t)))
(defun ebox--set-buffer-render-state (buffer root-node)
"Store ROOT-NODE and current render context for BUFFER."
(puthash buffer (ebox--make-buffer-render-state root-node)
ebox--buffer-render-state-table)
(ebox--buffer-viewport-dependent-node-id-axes buffer))
(defun ebox--buffer-node-table (buffer)
"Return BUFFER's runtime node table."
(plist-get (ebox--buffer-render-state buffer) :node-table))
(defun ebox--buffer-parent-table (buffer)
"Return BUFFER's runtime parent table."
(plist-get (ebox--buffer-render-state buffer) :parent-table))
(defun ebox--buffer-source-index (buffer)
"Return BUFFER's retained immutable source index."
(plist-get (ebox--buffer-render-state buffer) :source-index))
(defun ebox--buffer-region-id-set (buffer)
"Return BUFFER's runtime region id set."
(plist-get (ebox--buffer-render-state buffer) :region-id-set))
(defun ebox--buffer-region-node-table (buffer)
"Return BUFFER's region id to render-owner node id table."
(plist-get (ebox--buffer-render-state buffer) :region-node-table))
(defun ebox--buffer-region-box-table (buffer)
"Return BUFFER's region id to runtime box table."
(plist-get (ebox--buffer-render-state buffer) :region-box-table))
(defun ebox--buffer-host-ref-table (buffer)
"Return BUFFER's opaque host reference to runtime node id table."
(when-let* ((index
(plist-get (ebox--buffer-render-state buffer) :source-index)))
(ebox-source--index-host-ref-table-view index)))
(defun ebox-incremental--range-ref-resolve (state ref)
"Resolve REF in STATE to its parent, sequence, segment index, and rank."
(when-let* ((record (gethash ref (plist-get state :range-ref-table)))
(parent (gethash (plist-get record :parent-node-id)
(plist-get state :node-table)))
(sequence (plist-get parent :ebox-child-sequence)))
(list :parent parent :sequence sequence
:segment-index (plist-get record :segment-index)
:rank (ebox-child-range--rank
sequence (plist-get record :segment-index)))))
(defun ebox--buffer-region-render-owner-node-id (buffer region-id)
"Return REGION-ID's smallest render-owner node id in BUFFER.
Use the persistent runtime index when available. Legacy runtime states that
lack the index, or an indexed entry, fall back to a recursive tree lookup."
(or (when-let* ((table (ebox--buffer-region-node-table buffer)))
(gethash region-id table))
(when-let* ((root (ebox--buffer-root-node buffer))
(path (ebox--node-path-to-region root region-id)))
(ebox--ensure-node-id (car path)))))
(defun ebox--buffer-region-render-owner-node (buffer region-id)
"Return REGION-ID's smallest render-owner node in BUFFER.
Use both persistent runtime indexes in the normal path. Fall back to the
recursive region path only for legacy or incomplete runtime state."
(when-let* ((node-id
(ebox--buffer-region-render-owner-node-id buffer region-id)))
(ebox--buffer-runtime-node buffer node-id)))
(defun ebox--buffer-selector-id-table (buffer)
"Return BUFFER's runtime selector id index."
(plist-get (ebox-incremental--ensure-buffer-selector-indexes buffer)
:selector-id-table))
(defun ebox--buffer-selector-class-table (buffer)
"Return BUFFER's runtime selector class index."
(plist-get (ebox-incremental--ensure-buffer-selector-indexes buffer)
:selector-class-table))
(defun ebox--buffer-selector-type-table (buffer)
"Return BUFFER's runtime selector type index."
(plist-get (ebox-incremental--ensure-buffer-selector-indexes buffer)
:selector-type-table))
(defun ebox--buffer-render-cache (buffer)
"Return BUFFER's persistent render cache table."
(plist-get (ebox--buffer-render-state buffer) :render-cache))
(defun ebox--buffer-viewport-height-subtree-cache (buffer)
"Return BUFFER's persistent viewport-height subtree cache."
(plist-get (ebox--buffer-render-state buffer)
:viewport-height-dependent-subtree-cache))
(defun ebox--bump-buffer-runtime-revision
(buffer &optional preserve-reflow-prewarm-scratch)
"Increment and return BUFFER's runtime mutation revision.
Discard the isolated reflow scratch tree unless
PRESERVE-REFLOW-PREWARM-SCRATCH is non-nil. Callers may preserve it only
while validating one predicted root-width mutation against the new revision."
(when-let* ((state (ebox--buffer-render-state buffer)))
(let ((revision
(1+ (or (plist-get state :runtime-revision) 0))))
(plist-put state :runtime-revision revision)
(unless preserve-reflow-prewarm-scratch
(plist-put state :reflow-prewarm-scratch nil))
revision)))
(defun ebox--buffer-viewport-dependent-node-id-axes (buffer)
"Return cached viewport-dependent node ids by axis for BUFFER."
(when-let* ((state (ebox--buffer-render-state buffer)))
(if (and (plist-get state :viewport-dependent-node-ids-ready)
(plist-member state :viewport-dependent-node-id-axes))
(plist-get state :viewport-dependent-node-id-axes)
(let* ((root (plist-get state :root-node))
(axes (and root
(let ((ebox--viewport-dependent-node-ids-cache
(make-hash-table :test 'eq))
(ebox--viewport-height-dependent-subtree-cache
(or (ebox--buffer-viewport-height-subtree-cache
buffer)
(make-hash-table :test 'eq))))
(ebox--viewport-dependent-node-id-axes root)))))
(plist-put state :viewport-dependent-node-id-axes axes)
(plist-put state :viewport-dependent-node-ids
(and axes
(delete-dups
(copy-sequence
(append (car axes) (cdr axes))))))
(plist-put state :viewport-dependent-node-ids-ready t)
axes))))
(defun ebox--buffer-viewport-dependent-node-ids (buffer)
"Return cached viewport-dependent node ids for BUFFER."
(when-let* ((state (ebox--buffer-render-state buffer)))
(ebox--buffer-viewport-dependent-node-id-axes buffer)
(plist-get state :viewport-dependent-node-ids)))
(defun ebox--invalidate-buffer-viewport-dependencies (buffer)
"Invalidate BUFFER's cached viewport dependency list."
(when-let* ((state (ebox--buffer-render-state buffer)))
(plist-put state :viewport-dependent-node-ids nil)
(plist-put state :viewport-dependent-node-id-axes nil)
(plist-put state :viewport-dependent-node-ids-ready nil)))
(defun ebox--ensure-buffer-layout-snapshots (buffer)
"Return BUFFER's layout snapshot table, creating it when needed."
(let ((state (ebox--buffer-render-state buffer)))
(when state
(or (plist-get state :layout-snapshots)
(let ((snapshots (make-hash-table :test 'equal)))
(plist-put state :layout-snapshots snapshots)
snapshots)))))
(defconst ebox--layout-snapshot-detail-keys
'(:buffer-span :buffer-spans :line-signature
:span-footprint-signature :external-footprint-signature
:parent-slot-signature :role-topology-signature
:overflow-signature :detail-generation)
"Expensive layout snapshot detail keys that depend on current buffer spans.")
(defun ebox--layout-snapshot-buffer-spans-valid-p (buffer snapshot)
"Return non-nil when SNAPSHOT spans still fit BUFFER's live range."
(let ((spans (plist-get snapshot :buffer-spans)))
(or (null spans)
(and (buffer-live-p buffer)
(let ((minimum (with-current-buffer buffer (point-min)))
(maximum (with-current-buffer buffer (point-max))))
(cl-every
(lambda (span)
(and (consp span)
(integerp (car span))
(integerp (cdr span))
(<= minimum (car span))
(<= (car span) (cdr span))
(<= (cdr span) maximum)))
spans))))))
(defun ebox--buffer-layout-snapshot-detail-generation (buffer)
"Return BUFFER's current layout snapshot detail generation."
(or (plist-get (ebox--buffer-render-state buffer)
:layout-snapshot-detail-generation)
0))
(defun ebox--layout-snapshot-details-current-p (buffer snapshot)
"Return non-nil when SNAPSHOT's buffer-dependent detail fields are current."
(or (not (cl-some (lambda (key) (plist-member snapshot key))
ebox--layout-snapshot-detail-keys))
(and (= (or (plist-get snapshot :detail-generation) -1)
(ebox--buffer-layout-snapshot-detail-generation buffer))
(ebox--layout-snapshot-buffer-spans-valid-p buffer snapshot))))
(defun ebox--layout-snapshot (buffer node-id)
"Return BUFFER's layout snapshot for NODE-ID."
(when-let* ((snapshots (ebox--ensure-buffer-layout-snapshots buffer)))
(let ((snapshot (or (gethash node-id snapshots)
(when-let* ((node (ebox--snapshot-published-node
buffer node-id)))
(let ((created
(ebox--node-layout-snapshot
buffer node nil)))
(puthash node-id created snapshots)
created)))))
(when snapshot
(if (ebox--layout-snapshot-details-current-p buffer snapshot)
snapshot
(let ((stripped (ebox--layout-snapshot-strip-details snapshot)))
(puthash node-id stripped snapshots)
stripped))))))
(defvar ebox--region-line-index nil
"Dynamic snapshot-detail-local line/region index.
This is an optimization for completing snapshot details, not persistent state.")
(defvar ebox--region-line-index-buffer nil
"Dynamic snapshot-detail-local buffer used when a line index is available.")
(defmacro ebox--with-layout-snapshot-detail-context (buffer &rest body)
"Evaluate BODY with shared node caches for BUFFER snapshot detail capture.
This context is intentionally lazy and does not build a full region-line index.
Use `ebox--with-layout-snapshot-index-context' for batch snapshot capture."
(declare (indent 1) (debug t))
(let ((snapshot-buffer (make-symbol "snapshot-buffer")))
`(let ((,snapshot-buffer ,buffer))
(let ((ebox--region-line-index
ebox--region-line-index)
(ebox--region-line-index-buffer
(or ebox--region-line-index-buffer ,snapshot-buffer))
(ebox--node-region-ids-cache
(or ebox--node-region-ids-cache
(make-hash-table :test 'eq))))
,@body))))
(defmacro ebox--with-layout-snapshot-index-context (buffer &rest body)
"Evaluate BODY with shared node caches and an eager line index for BUFFER."
(declare (indent 1) (debug t))
(let ((snapshot-buffer (make-symbol "snapshot-buffer")))
`(let ((,snapshot-buffer ,buffer))
(let ((ebox--region-line-index
(or ebox--region-line-index
(and (buffer-live-p ,snapshot-buffer)
(with-current-buffer ,snapshot-buffer
(ebox--build-region-line-index)))))
(ebox--region-line-index-buffer ,snapshot-buffer)
(ebox--node-region-ids-cache
(or ebox--node-region-ids-cache
(make-hash-table :test 'eq))))
,@body))))
(defun ebox--current-layout-snapshots (buffer &optional details)
"Return a copy of BUFFER's current layout snapshot table.
When DETAILS is non-nil, include derived buffer spans and line signatures in
the returned copy without mutating BUFFER's stored snapshot table."
(when-let* ((state (ebox--buffer-render-state buffer)))
(when (and (plist-get state :root-node)
(not (plist-get state :layout-snapshots-complete-p)))
(ebox--refresh-buffer-layout-snapshots buffer details)))
(let ((copy (make-hash-table :test 'equal)))
(when-let* ((snapshots (ebox--ensure-buffer-layout-snapshots buffer)))
(let (entries)
(maphash (lambda (node-id snapshot)
(push (cons node-id
(if (ebox--layout-snapshot-details-current-p
buffer snapshot)
snapshot
(ebox--layout-snapshot-strip-details
snapshot)))
entries))
snapshots)
(if details
(ebox--with-layout-snapshot-index-context buffer
(dolist (entry entries)
(let* ((node-id (car entry))
(snapshot (cdr entry))
(node (ebox--buffer-runtime-node buffer node-id)))
(puthash node-id
(if node
(ebox--complete-layout-snapshot
buffer node snapshot t)
(copy-sequence snapshot))
copy))))
(dolist (entry entries)
(puthash (car entry)
(copy-sequence (cdr entry))
copy)))))
copy))
(defun ebox--put-layout-snapshot (buffer node-id snapshot)
"Store SNAPSHOT for NODE-ID in BUFFER render state."
(puthash node-id snapshot (ebox--ensure-buffer-layout-snapshots buffer)))
(defun ebox--clear-layout-snapshots (buffer)
"Clear BUFFER's layout snapshots."
(when-let* ((state (ebox--buffer-render-state buffer)))
(plist-put state :layout-snapshots-complete-p nil))
(when-let* ((snapshots (ebox--ensure-buffer-layout-snapshots buffer)))
(clrhash snapshots)))
(defun ebox--plist-remove-key (plist key)
"Return PLIST without KEY and its value."
(let (result)
(while plist
(let ((current-key (pop plist))
(value (pop plist)))
(unless (eq current-key key)
(push current-key result)
(push value result))))
(nreverse result)))
(defun ebox--layout-snapshot-strip-details (snapshot)
"Return SNAPSHOT with stale buffer-dependent detail fields removed."
(let ((result (copy-sequence snapshot)))
(dolist (key ebox--layout-snapshot-detail-keys result)
(setq result (ebox--plist-remove-key result key)))))
(defun ebox--clear-buffer-render-state (&optional buffer)
"Remove stored buffer-owned runtime state for BUFFER.
Defaults to the current buffer."
(let* ((buffer (or buffer (current-buffer)))
(state (gethash buffer ebox--buffer-render-state-table))
(native-session (plist-get state :native-sync-session)))
(when-let* ((cache (ebox--buffer-render-cache buffer)))
(when-let* ((root-cache
(gethash cache ebox--render-root-cache-table-table)))
(remhash root-cache ebox--render-cache-ring-table))
(remhash cache ebox--render-root-cache-table-table)
(remhash cache ebox--render-cache-ring-table))
(remhash buffer ebox--buffer-render-state-table)
(remhash buffer ebox-incremental--batch-table)
(ebox-cache-discard-report buffer)
(when (and native-session (require 'ebox-native-reflow nil t))
(ebox-native-reflow-release-session native-session))))
;;; Runtime Buffer Lookup
(defun ebox--prune-dead-buffer-render-states ()
"Remove killed buffers from buffer-owned runtime tables."
(let (dead-buffers)
(maphash
(lambda (buffer _state)
(unless (buffer-live-p buffer)
(push buffer dead-buffers)))
ebox--buffer-render-state-table)
(dolist (buffer dead-buffers)
(ebox--clear-buffer-render-state buffer))))
;;; Snapshot Dirty Patch Runtime
(defun ebox--region-id-set (region-ids)
"Return a hash set for REGION-IDS."
(let ((set (make-hash-table :test 'equal)))
(dolist (region-id region-ids)
(puthash region-id t set))
set))
(defun ebox--pos-region-ids (pos)
"Return all ebox region ids present at POS."
(let (ids)
(dolist (region-id (get-text-property pos 'ebox-content-owners))
(cl-pushnew region-id ids :test #'equal))
(dolist (entry ebox-region-types)
(when-let* ((region-id (get-text-property pos (cdr entry))))
(cl-pushnew region-id ids :test #'equal)))
ids))
(defun ebox--pos-region-role-ids (pos)
"Return all ebox region role/id pairs present at POS."
(let (role-ids)
(dolist (entry ebox-region-types)
(when-let* ((region-id (get-text-property pos (cdr entry))))
(push (cons (car entry) region-id) role-ids)))
role-ids))
(defun ebox--next-region-property-change (pos limit)
"Return the next ebox region property change after POS, capped at LIMIT."
(let ((next limit))
(dolist (entry ebox-region-types)
(let ((change (next-single-property-change
pos (cdr entry) nil limit)))
(when (< change next)
(setq next change))))
next))
(defun ebox--index-add-region-line-range
(region-lines region-id line-number start end)
"Record REGION-ID ownership from START to END on LINE-NUMBER."
(let ((line-map (or (gethash region-id region-lines)
(let ((map (make-hash-table :test 'eql)))
(puthash region-id map region-lines)
map))))
(if-let* ((range (gethash line-number line-map)))
(setcdr range (max (cdr range) end))
(puthash line-number (cons start end) line-map))))
(defun ebox--build-region-line-index ()
"Return a per-line index of ebox region ownership in the current buffer."
(let ((region-lines (make-hash-table :test 'equal))
(line-number 0)
lines)
(save-excursion
(goto-char (point-min))
(while (< (point) (point-max))
(let ((line-end (line-end-position))
segments)
(let ((pos (point)))
(while (< pos line-end)
(let* ((role-ids (ebox--pos-region-role-ids pos))
(ids (delete-dups
(mapcar #'cdr (copy-sequence role-ids))))
(next (ebox--next-region-property-change
pos line-end)))
(when ids
(push (list :start pos :end next
:ids ids :role-ids role-ids)
segments))
(setq pos (max next (1+ pos))))))
(setq segments (nreverse segments))
(dolist (segment segments)
(dolist (region-id (plist-get segment :ids))
(ebox--index-add-region-line-range
region-lines region-id line-number
(plist-get segment :start)
(plist-get segment :end))))
(push (list :segments segments) lines))
(setq line-number (1+ line-number))
(forward-line 1)))
(list :lines (vconcat (nreverse lines))
:region-lines region-lines)))
(defun ebox--build-region-line-index-for-spans (spans)
"Return a dense region ownership index limited to current buffer SPANS."
(let ((region-lines (make-hash-table :test 'equal))
(line-number 0)
lines)
(save-excursion
(dolist (span spans)
(let ((end (cdr span)))
(goto-char (car span))
(while (< (point) end)
(let ((line-end (min (line-end-position) end))
segments)
(let ((pos (point)))
(while (< pos line-end)
(let* ((role-ids (ebox--pos-region-role-ids pos))
(ids (delete-dups
(mapcar #'cdr (copy-sequence role-ids))))
(next (ebox--next-region-property-change
pos line-end)))
(when ids
(push (list :start pos :end next
:ids ids :role-ids role-ids)
segments))
(setq pos (max next (1+ pos))))))
(setq segments (nreverse segments))
(dolist (segment segments)
(dolist (region-id (plist-get segment :ids))
(ebox--index-add-region-line-range
region-lines region-id line-number
(plist-get segment :start)
(plist-get segment :end))))
(push (list :segments segments) lines)
(setq line-number (1+ line-number)))
(forward-line 1)))))
(list :lines (vconcat (nreverse lines))
:region-lines region-lines)))
(defun ebox--region-id-member-p (region-id region-set)
"Return non-nil when REGION-ID belongs to REGION-SET."
(gethash region-id region-set))
(defun ebox--region-ids-subset-p (region-ids region-set)
"Return non-nil when every REGION-IDS entry belongs to REGION-SET."
(cl-every (lambda (region-id)
(ebox--region-id-member-p region-id region-set))
region-ids))
(defun ebox--region-role-ids-compatible-p (role-ids region-set)
"Return non-nil when ROLE-IDS do not contain foreign blocking owners.
Wrapper and decoration roles may enclose a child patch span. A foreign direct
`content' owner means the candidate span crosses another editable box and is
therefore unsafe."
(cl-every (lambda (role-id)
(or (not (eq (car role-id) 'content))
(ebox--region-id-member-p (cdr role-id) region-set)))
role-ids))
(defun ebox--segment-overlaps-span-p (segment start end)
"Return non-nil when SEGMENT overlaps START..END."
(and (< (plist-get segment :start) end)
(> (plist-get segment :end) start)))
(defun ebox--region-set-line-spans-from-index (region-ids index)
"Return per-line spans for REGION-IDS using precomputed INDEX."
(let ((region-set (ebox--region-id-set region-ids))
(lines (plist-get index :lines))
(region-lines (plist-get index :region-lines))
(candidate-lines (make-hash-table :test 'eql))
spans)
(dolist (region-id region-ids)
(when-let* ((line-map (gethash region-id region-lines)))
(maphash
(lambda (line-number range)
(if-let* ((existing (gethash line-number candidate-lines)))
(progn
(setcar existing (min (car existing) (car range)))
(setcdr existing (max (cdr existing) (cdr range))))
(puthash line-number (copy-tree range) candidate-lines)))
line-map)))
(dolist (line-number
(sort (let (keys)
(maphash (lambda (key _value)
(push key keys))
candidate-lines)
keys)
#'<))
(let* ((span (gethash line-number candidate-lines))
(span-start (car span))
(span-end (cdr span))
(segments (plist-get (aref lines line-number) :segments))
(patchable t))
(dolist (segment segments)
(when (and patchable
(ebox--segment-overlaps-span-p
segment span-start span-end)
(not (ebox--region-role-ids-compatible-p
(or (plist-get segment :role-ids)
(mapcar (lambda (region-id)
(cons 'content region-id))
(plist-get segment :ids)))
region-set)))
(setq patchable nil)))
(when patchable
(push (cons span-start span-end) spans))))
(nreverse spans)))
(defun ebox--add-role-span-line-ranges (line-ranges span)
"Add per-line coverage from SPAN to LINE-RANGES."
(save-excursion
(let ((end (cdr span)))
(goto-char (car span))
(while (< (point) end)
(let* ((line-start (line-beginning-position))
(line-end (line-end-position))
(range-start (max (point) line-start))
(range-end (min end line-end)))
(when (< range-start range-end)
(if-let* ((existing (gethash line-start line-ranges)))
(progn
(setcar existing (min (car existing) range-start))
(setcdr existing (max (cdr existing) range-end)))
(puthash line-start (cons range-start range-end)
line-ranges)))
(forward-line 1))))))
(defun ebox--role-compatible-span-p (span region-set)
"Return non-nil when SPAN does not cross foreign content owners."
(let ((pos (car span))
(end (cdr span))
(compatible t))
(while (and compatible (< pos end))
(let ((next (or (next-single-property-change
pos 'ebox-content nil end)
end))
(content-owner (get-text-property pos 'ebox-content)))
(when (and content-owner
(not (ebox--region-id-member-p
content-owner region-set)))
(setq compatible nil))
(setq pos (max next (1+ pos)))))
compatible))
(defun ebox--region-set-line-spans-from-mounts (region-ids)
"Return per-line spans for REGION-IDS using TP-owned mounts."
(let ((region-set (ebox--region-id-set region-ids))
(line-ranges (make-hash-table :test 'eql))
spans)
(dolist (region-id region-ids)
(dolist (mount
(ebox-incremental--region-mounts
(current-buffer) region-id (mapcar #'car ebox-region-types)))
(ebox--add-role-span-line-ranges
line-ranges
(cons (plist-get mount :start) (plist-get mount :end)))))
(dolist (line-start
(sort (let (keys)
(maphash (lambda (key _value)
(push key keys))
line-ranges)
keys)
#'<))
(let ((span (gethash line-start line-ranges)))
(when (ebox--role-compatible-span-p span region-set)
(push (copy-tree span) spans))))
(nreverse spans)))
(defun ebox--region-set-line-spans (region-ids)
"Return patchable per-line spans owned by REGION-IDS.
If a line span would cross a foreign ebox region, return nil."
(if ebox--region-line-index
(ebox--region-set-line-spans-from-index
region-ids ebox--region-line-index)
(ebox--region-set-line-spans-from-mounts region-ids)))
(defun ebox--spans-line-signature (spans)
"Return pixel-width signature for current buffer SPANS."
(mapcar (lambda (span)
(ebox--string-pixel-width
(buffer-substring (car span) (cdr span))))
spans))
(defun ebox--rendered-line-signature (rendered)
"Return pixel-width signature for RENDERED."
(mapcar #'ebox--string-pixel-width (ebox-string-lines rendered)))
(defun ebox--safe-max-number (numbers)
"Return the maximum number in NUMBERS, or 0 for an empty list."
(if numbers
(apply #'max numbers)
0))
(defun ebox--span-whole-line-p (span)
"Return non-nil when SPAN covers the whole current buffer line."
(save-excursion
(goto-char (car span))
(and (= (car span) (line-beginning-position))
(= (cdr span) (line-end-position)))))
(defun ebox--spans-contiguous-lines-p (spans)
"Return non-nil when SPANS cover adjacent current buffer lines."
(or (null spans)
(cl-loop for rest on spans
while (cdr rest)
always (= (caadr rest) (1+ (cdar rest))))))
(defun ebox--span-footprint-signature
(span-count pixel-widths char-lengths &optional whole-line-flags
contiguous-lines)
"Return the normalized footprint signature for replaceable spans."
(list :span-count span-count
:line-pixel-widths pixel-widths
:char-lengths char-lengths
:whole-line-flags whole-line-flags
:contiguous-lines contiguous-lines))
(defun ebox--spans-span-footprint-signature (spans)
"Return the L2 span-footprint signature for current buffer SPANS."
(let ((pixel-widths (ebox--spans-line-signature spans))
char-lengths
whole-line-flags)
(dolist (span spans)
(push (- (cdr span) (car span)) char-lengths)
(push (ebox--span-whole-line-p span) whole-line-flags))
(ebox--span-footprint-signature
(length spans)
pixel-widths
(nreverse char-lengths)
(nreverse whole-line-flags)
(ebox--spans-contiguous-lines-p spans))))
(defun ebox--rendered-span-footprint-signature (rendered)
"Return the L2 span-footprint signature for RENDERED."
(let* ((lines (ebox-string-lines rendered))
(pixel-widths (mapcar #'ebox--string-pixel-width lines))
(char-lengths (mapcar #'length lines)))
(ebox--span-footprint-signature
(length lines)
pixel-widths
char-lengths
(make-list (length lines) t)
t)))
(defun ebox--external-footprint-signature-from-span-footprint (signature)
"Return parent-visible footprint facts derived from SIGNATURE."
(let ((pixel-widths (plist-get signature :line-pixel-widths)))
(list :block-lines (plist-get signature :span-count)
:max-line-pixel-width (ebox--safe-max-number pixel-widths))))
(defun ebox--span-parent-slot (span)
"Return parent-facing placement facts for SPAN in the current buffer."
(save-excursion
(goto-char (car span))
(let* ((line-start (line-beginning-position))
(start-pixel
(ebox--string-pixel-width
(buffer-substring line-start (car span))))
(width
(ebox--string-pixel-width
(buffer-substring (car span) (cdr span)))))
(list :line (line-number-at-pos (car span) t)
:start-pixel start-pixel
:end-pixel (+ start-pixel width)
:whole-line (ebox--span-whole-line-p span)))))
(defun ebox--spans-parent-slot-signature (spans)
"Return parent-facing slot signature for current buffer SPANS."
(list :slot-count (length spans)
:slots (mapcar #'ebox--span-parent-slot spans)))
(defun ebox--project-parent-slot-signature
(old-parent-slot new-span-footprint)
"Return the projected parent slot after applying NEW-SPAN-FOOTPRINT."
(let ((widths (plist-get new-span-footprint :line-pixel-widths))
projected-slots)
(cl-loop for slot in (plist-get old-parent-slot :slots)
for width in widths
do (push (plist-put (copy-sequence slot)
:end-pixel
(+ (plist-get slot :start-pixel) width))
projected-slots))
(list :slot-count (plist-get new-span-footprint :span-count)
:slots (nreverse projected-slots))))
(defun ebox--parent-slot-compatible-p (old new)
"Return non-nil when OLD and NEW parent slots are equivalent."
(equal old new))
(defun ebox--role-topology-signature (region-ids)
"Return the set of rendered region roles present for REGION-IDS."
(let ((role-set (make-hash-table :test 'eq))
roles)
(let ((roles (mapcar #'car ebox-region-types))
(buffer (current-buffer)))
(dolist (region-id region-ids)
(dolist (mount
(ebox-incremental--region-mounts buffer region-id roles))
(dolist (role (plist-get (plist-get mount :tags) :ebox/roles))
(puthash role t role-set)))))
(maphash (lambda (role _) (push role roles)) role-set)
(list :roles
(sort roles
(lambda (left right)
(string< (symbol-name left)
(symbol-name right)))))))
(defun ebox--overflow-signature (node)
"Return visible-overflow facts for NODE."
(list :visible (and node (ebox--node-visible-overflow-p node))))
(defun ebox--span-footprint-compatible-p (old new)
"Return non-nil when OLD and NEW span footprints are L2-compatible."
(and old new
(= (plist-get old :span-count)
(plist-get new :span-count))
(equal (plist-get old :line-pixel-widths)
(plist-get new :line-pixel-widths))))
(defun ebox--external-footprint-compatible-p (old new)
"Return non-nil when OLD and NEW parent-visible footprints match."
(equal old new))
(defun ebox--buffer-line-spans (buffer)
"Return whole-line spans for BUFFER."
(when (buffer-live-p buffer)
(with-current-buffer buffer
(let (spans)
(save-excursion
(goto-char (point-min))
(while (< (point) (point-max))
(push (cons (line-beginning-position)
(line-end-position))
spans)
(forward-line 1)))
(nreverse spans)))))
(defun ebox--complete-layout-snapshot-spans
(buffer node snapshot &optional copy)
"Return SNAPSHOT with buffer spans for NODE in BUFFER.
When COPY is non-nil, complete and return a copied snapshot without mutating
the original plist."
(let ((snapshot (if copy (copy-sequence snapshot) snapshot)))
(if (ebox--layout-snapshot-spans-p snapshot)
snapshot
(let* ((region-ids (or (plist-get snapshot :region-ids)
(ebox--node-all-region-ids node)))
(spans (if (equal (plist-get snapshot :node-id)
(ebox--buffer-root-node-id buffer))
(ebox--buffer-line-spans buffer)
(and region-ids
(buffer-live-p buffer)
(with-current-buffer buffer
(ebox--region-set-line-spans region-ids))))))
(setq snapshot (plist-put snapshot :region-ids region-ids))
(setq snapshot (plist-put snapshot :buffer-spans spans))
(setq snapshot
(plist-put snapshot :detail-generation
(ebox--buffer-layout-snapshot-detail-generation
buffer)))
snapshot))))
(defun ebox--complete-layout-snapshot (buffer node snapshot &optional copy)
"Return SNAPSHOT with derived spans and line signature for NODE in BUFFER.
When COPY is non-nil, complete and return a copied snapshot without mutating
the original plist."
(let ((snapshot (ebox--complete-layout-snapshot-spans
buffer node snapshot copy)))
(if (ebox--layout-snapshot-detailed-p snapshot)
snapshot
(let* ((spans (plist-get snapshot :buffer-spans))
(span-footprint
(and spans
(with-current-buffer buffer
(ebox--spans-span-footprint-signature spans))))
(line-signature
(plist-get span-footprint :line-pixel-widths))
(external-footprint
(and span-footprint
(ebox--external-footprint-signature-from-span-footprint
span-footprint)))
(parent-slot
(and spans
(with-current-buffer buffer
(ebox--spans-parent-slot-signature spans))))
(role-topology
(and (plist-get snapshot :region-ids)
(with-current-buffer buffer
(ebox--role-topology-signature
(plist-get snapshot :region-ids)))))
(overflow (ebox--overflow-signature node)))
(setq snapshot (plist-put snapshot :line-signature line-signature))
(setq snapshot
(plist-put snapshot
:span-footprint-signature span-footprint))
(setq snapshot
(plist-put snapshot
:external-footprint-signature external-footprint))
(setq snapshot
(plist-put snapshot :parent-slot-signature parent-slot))
(setq snapshot
(plist-put snapshot :role-topology-signature role-topology))
(setq snapshot
(plist-put snapshot :overflow-signature overflow))
snapshot))))
(defun ebox--snapshot-published-node (buffer node-id)
"Return the node whose PUBLISHED rendering a snapshot must describe.
During a logical-candidate planning pass the runtime lookup resolves
path-copied candidate nodes whose children already reflect the next
tree, but stored layout snapshots describe the still-published
buffer. Completing a snapshot from a candidate node drops regions
the candidate no longer contains, producing spans with holes and a
misplaced text replacement, so the base state's node wins whenever
the planning pass has one for this buffer."
(or (when-let* ((base ebox-incremental--candidate-base-state)
((eq buffer (car base)))
(node-table (plist-get (cdr base) :node-table)))
(gethash node-id node-table))
(ebox--buffer-runtime-node buffer node-id)))
(defun ebox--ensure-layout-snapshot-spans (buffer node-id)
"Ensure BUFFER's stored snapshot for NODE-ID has buffer span details."
(when-let* ((snapshot (ebox--layout-snapshot buffer node-id)))
(if (ebox--layout-snapshot-spans-p snapshot)
snapshot
(when-let* ((node (ebox--snapshot-published-node buffer node-id)))
(let ((detailed
(ebox--with-layout-snapshot-detail-context buffer
(ebox--complete-layout-snapshot-spans
buffer node snapshot))))
(ebox--put-layout-snapshot buffer node-id detailed)
detailed)))))
(defun ebox--ensure-layout-snapshot-details (buffer node-id)
"Ensure BUFFER's stored snapshot for NODE-ID has expensive detail fields."
(when-let* ((snapshot (ebox--layout-snapshot buffer node-id)))
(if (ebox--layout-snapshot-detailed-p snapshot)
snapshot
(when-let* ((node (ebox--snapshot-published-node buffer node-id)))
(let ((detailed
(ebox--with-layout-snapshot-detail-context buffer
(ebox--complete-layout-snapshot buffer node snapshot))))
(ebox--put-layout-snapshot buffer node-id detailed)
detailed)))))
(defun ebox--capture-layout-snapshots (buffer node &optional details)
"Capture derived layout snapshots for NODE's runtime tree in BUFFER.
When DETAILS is non-nil, include expensive per-line detail fields."
(when (and (buffer-live-p buffer)
(listp node))
(let ((node-id (ebox--ensure-node-id node)))
(ebox--put-layout-snapshot
buffer node-id (ebox--node-layout-snapshot buffer node details))
(dolist (child (ebox--node-children node))
(ebox--capture-layout-snapshots buffer child details)))))
(defun ebox--refresh-buffer-layout-snapshots (buffer &optional details)
"Recapture BUFFER's layout snapshots from its stored runtime root.
When DETAILS is non-nil, include expensive per-line detail fields."
(when-let* ((root (ebox--buffer-root-node buffer)))
(ebox--clear-layout-snapshots buffer)
(let ((ebox--node-region-ids-cache (make-hash-table :test 'eq)))
(if details
(ebox--with-layout-snapshot-index-context buffer
(ebox--capture-layout-snapshots buffer root t))
(ebox--capture-layout-snapshots buffer root)))
(when-let* ((state (ebox--buffer-render-state buffer)))
(plist-put state :layout-snapshots-complete-p t))))
(defconst ebox--dirty-kind-order
'(paint span geometry placement structure)
"Dirty kinds ordered from cheapest to most disruptive.")
(defun ebox--dirty-entry (node-id kind &rest props)
"Return a dirty-set entry for NODE-ID and KIND."
(append (list :node-id node-id :dirty-kind kind) props))
(defun ebox--dirty-set-kinds (dirty-set)
"Return ordered distinct dirty kinds from DIRTY-SET."
(let (kinds)
(dolist (entry dirty-set (nreverse kinds))
(let ((kind (plist-get entry :dirty-kind)))
(unless (memq kind kinds)
(push kind kinds))))))
(defun ebox--dirty-set-changed-keys (dirty-set)
"Return ordered distinct changed keys from DIRTY-SET."
(let (keys)
(dolist (entry dirty-set (nreverse keys))
(dolist (key (plist-get entry :changed-keys))
(unless (memq key keys)
(push key keys))))))
(defun ebox--dirty-set-impact-vector (dirty-set)
"Return ordered distinct impact facts from DIRTY-SET."
(let (impacts)
(dolist (entry dirty-set (nreverse impacts))
(dolist (impact (plist-get entry :impact-vector))
(unless (memq impact impacts)
(push impact impacts))))))
(defun ebox--constraint-change
(source owner-id owner-type root-node-id dirty-set &rest props)
"Return a normalized constraint-change descriptor."
(append (list :constraint-source source
:constraint-owner-id owner-id
:constraint-owner-type owner-type
:constraint-root-node-id root-node-id
:dirty-set dirty-set)
props))
(defun ebox--constraint-change-report-props (change)
"Return update-report properties describing CHANGE."
(let ((dirty-set (plist-get change :dirty-set)))
(list :constraint-source (plist-get change :constraint-source)
:constraint-owner-id (plist-get change :constraint-owner-id)
:constraint-owner-type (plist-get change :constraint-owner-type)
:constraint-root-node-id (plist-get change :constraint-root-node-id)
:dirty-kinds (ebox--dirty-set-kinds dirty-set)
:dirty-keys (ebox--dirty-set-changed-keys dirty-set)
:impact-vector (ebox--dirty-set-impact-vector dirty-set))))
(defun ebox--diff-layout-snapshots (old-table new-table)
"Return dirty-set entries comparing OLD-TABLE and NEW-TABLE."
(let (dirty)
(maphash
(lambda (node-id old)
(unless (gethash node-id new-table)
(push (ebox--dirty-entry node-id 'structure
:old old :new nil)
dirty)))
old-table)
(maphash
(lambda (node-id new)
(let* ((old (gethash node-id old-table))
(kind (ebox--layout-snapshot-dirty-kind old new)))
(when kind
(push (ebox--dirty-entry node-id kind
:old old :new new)
dirty))))
new-table)
(nreverse dirty)))
(defun ebox--runtime-node-by-id (node node-id)
"Return runtime NODE's descendant with NODE-ID."
(cond
((or (stringp node) (not (listp node))) nil)
((equal (ebox--ensure-node-id node) node-id) node)
(t
(cl-some (lambda (child)
(ebox--runtime-node-by-id child node-id))
(ebox--node-children node)))))
(defun ebox--buffer-runtime-node (buffer node-id)
"Return runtime node NODE-ID in BUFFER."
(or (and ebox-incremental--candidate-proof-node-table
(eq buffer
(car ebox-incremental--candidate-proof-node-table))
(gethash node-id
(cdr ebox-incremental--candidate-proof-node-table)))
(when-let* ((node-table (ebox--buffer-node-table buffer)))
(gethash node-id node-table))
(when-let* ((root (ebox--buffer-root-node buffer)))
(ebox--runtime-node-by-id root node-id))))
(defun ebox--runtime-parent-id (buffer node-id)
"Return NODE-ID's parent id in BUFFER runtime state."
(when-let* ((parent-table (ebox--buffer-parent-table buffer)))
(gethash node-id parent-table)))
(defun ebox--invalidate-runtime-render-signature-path
(node-id node-table parent-table signature-cache height-cache)
"Invalidate NODE-ID to root in explicit runtime structural caches."
(if (null node-id)
(progn
(when signature-cache (clrhash signature-cache))
(when height-cache (clrhash height-cache)))
(while node-id
(when-let* ((node (gethash node-id node-table)))
(when signature-cache (remhash node signature-cache))
(when height-cache (remhash node height-cache)))
(setq node-id (gethash node-id parent-table)))))
(defun ebox--runtime-ancestor-id-p (buffer ancestor-id node-id)
"Return non-nil when ANCESTOR-ID is NODE-ID's ancestor in BUFFER."
(let ((parent (ebox--runtime-parent-id buffer node-id))
found)
(while (and parent (not found))
(if (equal parent ancestor-id)
(setq found t)
(setq parent (ebox--runtime-parent-id buffer parent))))
found))
(defun ebox--patch-op (op owner-id &rest props)
"Return a patch-set operation."
(append (list :op op :owner-id owner-id) props))
(defvar ebox--patchable-owner-cache nil
"Dynamic patch-planning cache for `ebox--patchable-owner-p' results.")
(defvar ebox--runtime-ancestor-cache nil
"Dynamic patch-planning cache for runtime ancestor checks.")
(defvar ebox--flex-slot-safety-cache nil
"Dynamic patch-planning cache for flex item slot safety checks.")
(defconst ebox--owner-coalesce-min-count 8
"Minimum owner-rerender op count before considering root coalescing.")
(defconst ebox--owner-coalesce-min-coverage 0.8
"Minimum root line coverage ratio before coalescing local owner patches.")
(defconst ebox-incremental--broad-dirty-min-node-coverage 0.8
"Minimum dirty descendant ratio for broad owner pre-coalescing.")
(defconst ebox--span-patch-local-style-keys
'(:content :text-align :vertical-align :wrap-mode :scroll-offset
:visibility)
"Geometry style keys that can be local span-patch candidates.
Visibility toggles rerender the owner's own spans with ink suppressed
or restored while the footprint stays identical, so they are local by
construction.")
(defconst ebox--span-patch-border-box-inline-keys
'(:padding-left-pixel :padding-right-pixel
:border-left-pixel :border-right-pixel)
"Border-box inline keys that can be internal span-patch candidates.")
(defconst ebox--span-patch-border-box-block-keys
'(:padding-top-height :padding-bottom-height)
"Border-box block keys that can be internal span-patch candidates.")
(defun ebox--cached-patchable-owner-p
(buffer node-id &optional dirty-kind allow-owned-overflow-coverage)
"Return whether NODE-ID is patchable, using a patch-planning cache."
(if (not ebox--patchable-owner-cache)
(ebox--patchable-owner-p
buffer node-id dirty-kind allow-owned-overflow-coverage)
(let* ((key (list node-id dirty-kind allow-owned-overflow-coverage))
(missing (make-symbol "ebox-patchable-missing"))
(cached (gethash key ebox--patchable-owner-cache missing)))
(if (not (eq cached missing))
cached
(puthash key
(ebox--patchable-owner-p
buffer node-id dirty-kind allow-owned-overflow-coverage)
ebox--patchable-owner-cache)))))
(defun ebox--runtime-ancestor-id-set (buffer node-id)
"Return NODE-ID's complete strict-ancestor id set in BUFFER.
Uses the planning cache when bound so one parent walk serves every
later membership check for the same node."
(or (and ebox--runtime-ancestor-cache
(gethash node-id ebox--runtime-ancestor-cache))
(let ((ancestors (make-hash-table :test 'equal))
(walk (ebox--runtime-parent-id buffer node-id)))
(while walk
(puthash walk t ancestors)
(setq walk (ebox--runtime-parent-id buffer walk)))
(when ebox--runtime-ancestor-cache
(puthash node-id ancestors ebox--runtime-ancestor-cache))
ancestors)))
(defun ebox--cached-runtime-ancestor-id-p (buffer ancestor-id node-id)
"Return whether ANCESTOR-ID covers NODE-ID, using a planning cache.
The cache stores one complete ancestor-id set per node, built by a
single parent walk, so a planning pass with many pairwise dominance
checks pays one walk per distinct node instead of one walk per pair."
(if (not ebox--runtime-ancestor-cache)
(ebox--runtime-ancestor-id-p buffer ancestor-id node-id)
(gethash ancestor-id
(ebox--runtime-ancestor-id-set buffer node-id))))
(defun ebox--patch-operation-strength (strategy)
"Return structural dominance strength for patch STRATEGY."
(pcase strategy
('native-frame 4)
('owner-rerender 3)
('span-patch 2)
('paint-patch 1)
('child-splice 0)
('child-reorder 0)
(_ 0)))
(defun ebox--dirty-kind-strength (kind)
"Return relative disruption strength for dirty KIND."
(or (cl-position kind ebox--dirty-kind-order) -1))
(defun ebox--dirty-provenance-node-ids (dirty)
"Return the distinct source node ids recorded by DIRTY provenance."
(delete-dups
(append (when-let* ((node-id (plist-get dirty :node-id)))
(list node-id))
(copy-sequence (or (plist-get dirty :node-ids) nil)))))
(defun ebox--merge-dirty-provenance (primary secondary)
"Merge SECONDARY dirty provenance into PRIMARY."
(cond
((null primary) (copy-sequence secondary))
((null secondary) (copy-sequence primary))
(t
(let ((merged (copy-sequence primary)))
(setq merged
(plist-put
merged :node-ids
(delete-dups
(append (ebox--dirty-provenance-node-ids primary)
(ebox--dirty-provenance-node-ids secondary)))))
(dolist (key '(:changed-keys :region-ids :old-region-ids
:new-region-ids :impact-vector))
(setq merged
(plist-put
merged key
(delete-dups
(append (copy-sequence (or (plist-get primary key) nil))
(copy-sequence (or (plist-get secondary key) nil)))))))
(when (or (plist-get primary :requires-owned-overflow-coverage)
(plist-get secondary :requires-owned-overflow-coverage))
(setq merged
(plist-put merged :requires-owned-overflow-coverage t)))
(when (> (ebox--dirty-kind-strength
(plist-get secondary :dirty-kind))
(ebox--dirty-kind-strength
(plist-get primary :dirty-kind)))
(setq merged
(plist-put merged :dirty-kind
(plist-get secondary :dirty-kind))))
merged))))
(defun ebox--merge-dirty-provenance-list (entries)
"Merge dirty provenance from ENTRIES into one entry."
(when entries
(cl-reduce #'ebox--merge-dirty-provenance entries)))
(defun ebox--patch-op-merge-dirty (op absorbed)
"Return OP with dirty provenance from ABSORBED merged into it."
(let ((merged (copy-sequence op)))
(plist-put merged :dirty
(ebox--merge-dirty-provenance
(plist-get op :dirty)
(plist-get absorbed :dirty)))))
(defun ebox--merge-same-owner-patch-ops (left right)
"Return the stronger same-owner patch with merged dirty provenance."
(let* ((left-strength
(ebox--patch-operation-strength (plist-get left :op)))
(right-strength
(ebox--patch-operation-strength (plist-get right :op)))
(left-dirty-strength
(ebox--dirty-kind-strength
(plist-get (plist-get left :dirty) :dirty-kind)))
(right-dirty-strength
(ebox--dirty-kind-strength
(plist-get (plist-get right :dirty) :dirty-kind)))
(left-wins
(or (> left-strength right-strength)
(and (= left-strength right-strength)
(>= left-dirty-strength right-dirty-strength))))
(winner (if left-wins left right))
(loser (if left-wins right left))
(merged (copy-sequence winner)))
(plist-put merged :dirty
(ebox--merge-dirty-provenance
(plist-get winner :dirty)
(plist-get loser :dirty)))))
(defun ebox--patch-op-covers-node-p (buffer op node-id node-strategy)
"Return non-nil when OP dominates NODE-STRATEGY at NODE-ID in BUFFER."
(let* ((owner-id (plist-get op :owner-id))
(op-strength
(ebox--patch-operation-strength (plist-get op :op))))
(and owner-id node-id
(if (equal owner-id node-id)
(>= op-strength
(ebox--patch-operation-strength node-strategy))
(and (>= op-strength
(ebox--patch-operation-strength 'span-patch))
(or (equal owner-id (ebox--buffer-root-node-id buffer))
(ebox--cached-runtime-ancestor-id-p
buffer owner-id node-id)))))))
(defun ebox--patch-op-dominates-p (buffer op other)
"Return non-nil when OP makes OTHER redundant in BUFFER."
(ebox--patch-op-covers-node-p
buffer op (plist-get other :owner-id) (plist-get other :op)))
(defun ebox--merge-patch-op-into-set (buffer ops candidate)
"Merge CANDIDATE into OPS using operation-aware coverage in BUFFER."
(let* ((owner-id (plist-get candidate :owner-id))
(same-owner
(cl-find-if (lambda (op)
(equal (plist-get op :owner-id) owner-id))
ops))
(candidate (if same-owner
(ebox--merge-same-owner-patch-ops
same-owner candidate)
candidate))
(remaining (if same-owner
(delq same-owner (copy-sequence ops))
(copy-sequence ops))))
(if-let* ((dominator
(cl-find-if
(lambda (op)
(ebox--patch-op-dominates-p buffer op candidate))
remaining)))
(mapcar (lambda (op)
(if (eq op dominator)
(ebox--patch-op-merge-dirty op candidate)
op))
remaining)
(let ((survivors nil))
(dolist (op remaining)
(if (ebox--patch-op-dominates-p buffer candidate op)
(setq candidate (ebox--patch-op-merge-dirty candidate op))
(push op survivors)))
(append (nreverse survivors) (list candidate))))))
(defun ebox--span-patch-definite-size-value-p (value)
"Return non-nil when VALUE is definite enough for local span planning."
(and value
(not (memq value '(auto min-content max-content fit-content
stretch contain viewport)))
(not (ebox--viewport-dependent-size-value-p value))))
(defun ebox--dirty-entry-local-span-keys-p (entry)
"Return non-nil when ENTRY changed only local span candidate keys.
Paint-class keys ride along: a span rerender republishes the owner's
text together with its faces and surface text properties, so mixing
`:surface-properties' or paint signature keys with local span keys
keeps the change local to the owner's own rendered spans."
(let ((changed-keys (plist-get entry :changed-keys)))
(and changed-keys
(cl-every (lambda (key)
(or (memq key ebox--span-patch-local-style-keys)
(eq key :surface-properties)
(memq key ebox--paint-style-signature-keys)
(memq key ebox--typography-style-signature-keys)))
changed-keys))))
(defun ebox--border-box-inline-content-fits-p (style-node)
"Return non-nil when STYLE-NODE's inline chrome fits its fixed width."
(let ((width (ebox-get style-node :width)))
(and (numberp width)
(>= width
(+ (ebox-get style-node :padding-left-pixel)
(ebox-get style-node :padding-right-pixel)
(ebox-get style-node :border-left-pixel)
(ebox-get style-node :border-right-pixel))))))
(defun ebox--border-box-block-content-fits-p (style-node)
"Return non-nil when STYLE-NODE's block padding leaves one content line."
(let ((height (ebox-get style-node :height)))
(and (numberp height)
(>= height
(+ 1
(floor (ebox-get style-node :padding-top-height))
(floor (ebox-get style-node :padding-bottom-height)))))))
(defun ebox--dirty-entry-border-box-internal-span-p (buffer entry)
"Return non-nil when ENTRY changes only fixed border-box internals.
Inline and block padding/border fields may arrive together after shorthand
expansion. Treat that mixed set as one local candidate when the corresponding
outer width and height are definite and still contain their inner chrome."
(let* ((changed-keys (plist-get entry :changed-keys))
(inline-changed
(cl-some (lambda (key)
(memq key ebox--span-patch-border-box-inline-keys))
changed-keys))
(block-changed
(cl-some (lambda (key)
(memq key ebox--span-patch-border-box-block-keys))
changed-keys)))
(and changed-keys
(cl-every
(lambda (key)
(or (memq key ebox--span-patch-border-box-inline-keys)
(memq key ebox--span-patch-border-box-block-keys)))
changed-keys)
(when-let* ((node-id (plist-get entry :node-id))
(node (ebox--buffer-runtime-node buffer node-id))
(style-node (ebox-fragment-style-source-node node)))
(and (eq (ebox-get style-node :box-sizing) 'border-box)
(or (not inline-changed)
(ebox--border-box-inline-content-fits-p style-node))
(or (not block-changed)
(ebox--border-box-block-content-fits-p style-node)))))))
(defun ebox--dirty-entry-span-patch-candidate-p (buffer entry)
"Return non-nil when ENTRY can first attempt a local span patch."
(and buffer
(eq (plist-get entry :dirty-kind) 'geometry)
(or (ebox--dirty-entry-local-span-keys-p entry)
(ebox--dirty-entry-border-box-internal-span-p buffer entry))))
(defun ebox--changed-key-impact (key style-node)
"Return impact facts for changed KEY on STYLE-NODE."
(cond
((memq key ebox--paint-style-signature-keys)
'(paint))
;; Surface text properties are stamped onto the owner's rendered
;; spans and cannot move any geometry, exactly like paint; the
;; declarative dirty-kind classifier already groups them with paint.
((eq key :surface-properties)
'(paint))
((eq key :content)
'(internal-fragment external-footprint-proof))
;; Typography is local to the rendered fragment but may change its pixel
;; footprint. Authorize the same exact-footprint attempt as content;
;; Surface rejects wrapping or allocation changes before publication.
((memq key ebox--typography-style-signature-keys)
'(internal-fragment external-footprint-proof))
((memq key ebox--span-patch-local-style-keys)
'(internal-fragment))
((and (eq (ebox-get style-node :box-sizing) 'border-box)
(memq key ebox--span-patch-border-box-inline-keys)
(ebox--span-patch-definite-size-value-p
(ebox-get style-node :width)))
'(internal-fragment external-footprint-proof))
((and (eq (ebox-get style-node :box-sizing) 'border-box)
(memq key ebox--span-patch-border-box-block-keys)
(numberp (ebox-get style-node :height)))
'(internal-fragment external-footprint-proof))
((eq key :overflow)
'(clipping external-footprint))
((eq key :box-sizing)
'(size-interpretation external-footprint))
((memq key '(:margin-left-pixel :margin-right-pixel
:margin-top-height :margin-bottom-height))
'(external-footprint parent-layout))
(t
'(external-footprint parent-layout))))
(defun ebox--dirty-entry-geometry-owner-id (buffer entry)
"Return ENTRY's node that owns geometry in BUFFER.
Text retains content identity and paint provenance, while its containing Box
owns the allocated layout slot. Keep the dirty entry addressed to Text, but
classify and prove a Text content change against that parent Box."
(let* ((source-node-id (plist-get entry :node-id))
(source-node (and source-node-id
(ebox--buffer-runtime-node buffer source-node-id))))
(if (and (eq (plist-get entry :dirty-kind) 'geometry)
(eq (plist-get source-node :ebox-kind) 'text)
(memq :content (plist-get entry :changed-keys)))
(or (ebox--runtime-parent-id buffer source-node-id)
source-node-id)
source-node-id)))
(defun ebox--dirty-entry-impact-vector (buffer entry)
"Return normalized impact vector for dirty ENTRY in BUFFER."
(let* ((node-id (ebox--dirty-entry-geometry-owner-id buffer entry))
(node (and node-id (ebox--buffer-runtime-node buffer node-id)))
(style-node (and node (ebox-fragment-style-source-node node)))
impacts)
(dolist (key (plist-get entry :changed-keys))
(dolist (impact (ebox--changed-key-impact key style-node))
(cl-pushnew impact impacts)))
;; An auto-width content box contributes its intrinsic width to the
;; enclosing stack. Its text change can therefore move every later
;; sibling even when the local box remains span-patchable. Equal-length
;; candidates may reach the final footprint proof; that proof still
;; rejects pixel-width, wrapping, or unstable parent-allocation changes.
(when (and style-node
(memq :content (plist-get entry :changed-keys))
(not (ebox--span-patch-definite-size-value-p
(ebox-get style-node :width)))
(not (let ((old-content
(plist-get (plist-get entry :old-signature)
:content))
(new-content
(plist-get (plist-get entry :new-signature)
:content)))
(and (stringp old-content)
(stringp new-content)
(= (length old-content) (length new-content)))))
(not (and ebox-incremental--candidate-state-for-slot-proof
(or
(ebox-incremental--fast-grid-slot-evidence
buffer entry
ebox-incremental--candidate-state-for-slot-proof)
(ebox-incremental--allocated-parent-slot-stable-p
buffer entry
ebox-incremental--candidate-state-for-slot-proof)))))
(cl-pushnew 'parent-layout impacts))
(nreverse impacts)))
(defun ebox--dirty-entry-requires-owned-overflow-coverage-p (buffer entry)
"Return non-nil when ENTRY changes to or from unowned visible overflow."
(when (memq :overflow (plist-get entry :changed-keys))
(let* ((node-id (plist-get entry :node-id))
(node (and node-id (ebox--buffer-runtime-node buffer node-id)))
(style-node (and node (ebox-fragment-style-source-node node)))
(old-values (plist-get entry :old-style-values))
(stored-snapshots
(plist-get (ebox--buffer-render-state buffer) :layout-snapshots))
(old-snapshot
(or (plist-get entry :old)
(and stored-snapshots node-id
(gethash node-id stored-snapshots))))
(old-style (and old-snapshot
(plist-get old-snapshot :style-signature)))
(old-overflow-known
(or (plist-member old-values :overflow)
(and old-style (plist-member old-style :overflow))))
(old-overflow
(if (plist-member old-values :overflow)
(plist-get old-values :overflow)
(plist-get old-style :overflow)))
(new-overflow (and style-node (ebox-get style-node :overflow))))
(or (not old-overflow-known)
(eq old-overflow 'visible)
(eq new-overflow 'visible)))))
(defun ebox--dirty-entry-with-impact-vector (buffer entry)
"Return ENTRY with an impact vector computed in BUFFER."
(setq entry
(plist-put entry :impact-vector
(ebox--dirty-entry-impact-vector buffer entry)))
(when (ebox--dirty-entry-requires-owned-overflow-coverage-p buffer entry)
(setq entry
(plist-put entry :requires-owned-overflow-coverage t)))
entry)
(defun ebox--impact-vector-requires-parent-promotion-p (impact-vector)
"Return non-nil when IMPACT-VECTOR requires parent-layout promotion."
(or (null impact-vector)
(memq 'parent-layout impact-vector)
(memq 'clipping impact-vector)
(memq 'size-interpretation impact-vector)))
(defun ebox--impact-vector-requires-conservative-owner-p (impact-vector)
"Return non-nil when IMPACT-VECTOR must use conservative owner rerendering."
(or (memq 'clipping impact-vector)
(memq 'size-interpretation impact-vector)))
(defun ebox--node-own-overflow-visible-p (buffer node-id)
"Return non-nil when NODE-ID's current or stored own overflow is visible."
(let* ((node (ebox--buffer-runtime-node buffer node-id))
(style-node (and node (ebox-fragment-style-source-node node)))
(snapshots
(plist-get (ebox--buffer-render-state buffer) :layout-snapshots))
(snapshot (and snapshots (gethash node-id snapshots)))
(old-style (and snapshot
(plist-get snapshot :style-signature))))
(or (eq (and style-node (ebox-get style-node :overflow)) 'visible)
(eq (plist-get old-style :overflow) 'visible))))
(defun ebox--ancestor-own-overflow-visible-p (buffer node-id)
"Return non-nil when an ancestor of NODE-ID has visible own overflow."
(let ((ancestor-id (ebox--runtime-parent-id buffer node-id))
found)
(while (and ancestor-id (not found))
(if (ebox--node-own-overflow-visible-p buffer ancestor-id)
(setq found t)
(setq ancestor-id (ebox--runtime-parent-id buffer ancestor-id))))
found))
(defun ebox--owned-overflow-coverage-wrapper-node-p (buffer node-id)
"Return non-nil when NODE-ID's wrapper owns the required overflow footprint."
(when-let* ((node (ebox--buffer-runtime-node buffer node-id))
(style-node (ebox-fragment-style-source-node node)))
(let* ((parent-id (ebox--runtime-parent-id buffer node-id))
(parent (and parent-id
(ebox--buffer-runtime-node buffer parent-id))))
(and (ebox-get style-node :region-id)
(not (eq (ebox-get style-node :overflow) 'visible))
(not (ebox--ancestor-own-overflow-visible-p buffer node-id))
(not (and parent
(eq (ebox--display-inner parent) 'flex)))))))
(defun ebox--patch-op-from-dirty-entry (entry &optional buffer)
"Return the patch op for dirty-set ENTRY.
When BUFFER is non-nil, non-paint entries promote to patchable owners."
(let* ((node-id (plist-get entry :node-id))
(dirty-kind (plist-get entry :dirty-kind))
(geometry-owner-id
(if (and buffer (eq dirty-kind 'geometry))
(ebox--dirty-entry-geometry-owner-id buffer entry)
node-id))
(requires-owned-overflow-coverage
(or (plist-get entry :requires-owned-overflow-coverage)
(and buffer
(ebox--dirty-entry-requires-owned-overflow-coverage-p
buffer entry))))
(entry
(if requires-owned-overflow-coverage
(plist-put entry :requires-owned-overflow-coverage t)
entry))
(impact-vector (or (plist-get entry :impact-vector)
(and buffer
(plist-get entry :changed-keys)
(ebox--dirty-entry-impact-vector
buffer entry)))))
(pcase dirty-kind
('paint
(ebox--patch-op 'paint-patch node-id
:dirty entry))
(_
(if-let* ((span-owner-id
(and buffer
(ebox--span-patch-owner-id-for-dirty-entry
entry buffer))))
(ebox--patch-op 'span-patch span-owner-id
:dirty entry)
(let ((fallback-owner-id
(or (and buffer
requires-owned-overflow-coverage
(or (ebox--next-patchable-owner-ancestor
buffer geometry-owner-id dirty-kind t)
(ebox--buffer-root-node-id buffer)))
(and buffer
(or (memq 'size-interpretation impact-vector)
(not
(ebox--impact-vector-requires-conservative-owner-p
impact-vector)))
(ebox--promote-to-patchable-owner
buffer geometry-owner-id dirty-kind
(plist-get entry :changed-keys)
impact-vector))
(and buffer (ebox--buffer-root-node-id buffer))
node-id)))
(ebox--patch-op 'owner-rerender fallback-owner-id
:dirty entry)))))))
(defun ebox--line-spans-cover-whole-lines-p (spans)
"Return non-nil when every span in SPANS owns whole buffer lines."
(cl-every (lambda (span)
(save-excursion
(goto-char (car span))
(and (= (car span) (line-beginning-position))
(= (cdr span) (line-end-position)))))
spans))
(defun ebox--row-or-flex-layout-p (node)
"Return non-nil when NODE repositions siblings after child geometry changes."
(memq (ebox--display-inner node) '(row flex)))
(defun ebox--row-or-flex-ancestor-p (buffer node-id)
"Return non-nil when NODE-ID is contained by a row or flex layout ancestor."
(let ((ancestor-id (ebox--runtime-parent-id buffer node-id))
found)
(while (and ancestor-id (not found))
(let ((ancestor (ebox--buffer-runtime-node buffer ancestor-id)))
(if (and ancestor (ebox--row-or-flex-layout-p ancestor))
(setq found t)
(setq ancestor-id
(ebox--runtime-parent-id buffer ancestor-id)))))
found))
(defun ebox--geometry-affects-parent-layout-p (buffer node-id)
"Return non-nil when NODE-ID geometry can change parent layout in BUFFER."
(when-let* ((parent-id (ebox--runtime-parent-id buffer node-id))
(parent (ebox--buffer-runtime-node buffer parent-id)))
(or (ebox--row-or-flex-layout-p parent)
(ebox--row-or-flex-ancestor-p buffer parent-id)
(not (ebox--definite-containment-formatting-context-p
buffer parent-id)))))
(defun ebox--definite-containment-inline-size-value-p (value)
"Return non-nil when VALUE is independent of intrinsic inline content."
(and (ebox--span-patch-definite-size-value-p value)
(not (eq (car-safe value) 'fit-content))))
(defun ebox--inline-size-change-p (changed-keys)
"Return non-nil when CHANGED-KEYS contains only inline-size fields."
(and changed-keys
(cl-every (lambda (key)
(memq key '(:width :min-width :max-width)))
changed-keys)))
(defun ebox--definite-containment-formatting-context-p (buffer node-id)
"Return non-nil when NODE-ID contains descendant inline size in BUFFER.
A formatting context is a safe local owner candidate only when both outer axes
are definite and neither it nor a descendant allows visible overflow. Backend
footprint and parent-slot checks still decide whether it may actually be
patched."
(when-let* ((node (ebox--buffer-runtime-node buffer node-id))
((ebox--formatting-context-p node))
(style-node (ebox-fragment-style-source-node node)))
(let ((min-width (ebox-get style-node :min-width))
(max-width (ebox-get style-node :max-width))
(min-height (ebox-get style-node :min-height))
(max-height (ebox-get style-node :max-height)))
(and (ebox--definite-containment-inline-size-value-p
(ebox-get style-node :width))
(or (null min-width)
(ebox--definite-containment-inline-size-value-p min-width))
(or (null max-width)
(eq max-width 'none)
(ebox--definite-containment-inline-size-value-p max-width))
(numberp (ebox-get style-node :height))
(or (null min-height) (numberp min-height))
(or (null max-height)
(eq max-height 'none)
(numberp max-height))
(not (eq (ebox-get style-node :overflow) 'visible))
(not (ebox--node-visible-overflow-p node))))))
(defun ebox--direct-row-or-flex-parent-p (buffer node-id)
"Return non-nil when NODE-ID is a direct row/flex item owner."
(when-let* ((parent-id (ebox--runtime-parent-id buffer node-id))
(parent (ebox--buffer-runtime-node buffer parent-id)))
(ebox--row-or-flex-layout-p parent)))
(defun ebox--direct-flex-parent-p (buffer node-id)
"Return non-nil when NODE-ID is a direct flex item owner."
(when-let* ((parent-id (ebox--runtime-parent-id buffer node-id))
(parent (ebox--buffer-runtime-node buffer parent-id)))
(eq (ebox--display-inner parent) 'flex)))
(defconst ebox--flex-participation-style-keys
'(:flex :flex-grow :flex-shrink :flex-basis :order :align-self
:flex-participation)
"Keys that participate in a parent flex sizing or placement algorithm.")
(defun ebox--flex-main-axis (flex-node)
"Return FLEX-NODE's main axis."
(if (memq (plist-get (plist-get flex-node :props) :flex-direction)
'(column column-reverse))
'column
'row))
(defun ebox--flex-main-definite-size-p (style-node axis)
"Return non-nil when STYLE-NODE has a definite main-axis outer size."
(and style-node
(if (eq axis 'row)
(ebox--span-patch-definite-size-value-p
(ebox-get style-node :width))
(numberp (ebox-get style-node :height)))))
(defun ebox--flex-border-box-main-internal-key-p (key axis)
"Return non-nil when KEY can be internal to a fixed border-box main size."
(if (eq axis 'row)
(memq key '(:padding-left-pixel :padding-right-pixel
:border-left-pixel :border-right-pixel))
(memq key '(:padding-top-height :padding-bottom-height))))
(defun ebox--flex-content-flow-main-key-p (key)
"Return non-nil when KEY can change an auto/content flex base size."
(memq key '(:content :wrap-mode)))
(defun ebox--flex-main-footprint-key-p (key axis style-node)
"Return non-nil when KEY can change STYLE-NODE's flex main footprint."
(or (memq key ebox--flex-participation-style-keys)
(eq key :box-sizing)
(if (eq axis 'row)
(memq key '(:width :min-width :max-width
:margin-left-pixel :margin-right-pixel))
(memq key '(:height :min-height :max-height
:margin-top-height :margin-bottom-height)))
(and (ebox--flex-border-box-main-internal-key-p key axis)
(not (and (eq (ebox-get style-node :box-sizing) 'border-box)
(ebox--flex-main-definite-size-p style-node axis))))
(and (ebox--flex-content-flow-main-key-p key)
(not (ebox--flex-main-definite-size-p style-node axis)))))
(defun ebox--flex-main-size-keys-p (buffer node-id changed-keys)
"Return non-nil when CHANGED-KEYS can change NODE-ID's flex main footprint."
(and changed-keys
(when-let* ((parent-id (ebox--runtime-parent-id buffer node-id))
(parent (ebox--buffer-runtime-node buffer parent-id))
((eq (ebox--display-inner parent) 'flex))
(node (ebox--buffer-runtime-node buffer node-id))
(style-node (ebox-fragment-style-source-node node)))
(let ((axis (ebox--flex-main-axis parent)))
(cl-some (lambda (key)
(ebox--flex-main-footprint-key-p
key axis style-node))
changed-keys)))))
(defun ebox--flex-item-slot-at-declared-main-p (buffer node-id)
"Return non-nil when NODE-ID's flex slot equals its declared main size.
Such an allocation is anchored by the item's own definite size rather
than by its content minimum, so a descendant footprint change cannot
move the slot while free space and sibling bases stay untouched. A
slot above or below the declared size was decided by grow, shrink, or
a content-minimum clamp, and descendant changes may move it."
(when-let* ((node (ebox--buffer-runtime-node buffer node-id))
(parent-id (ebox--runtime-parent-id buffer node-id))
(parent (ebox--buffer-runtime-node buffer parent-id))
((eq (ebox--display-inner parent) 'flex))
(style-node (ebox-fragment-style-source-node node))
(snapshot (ebox--ensure-layout-snapshot-spans buffer node-id))
(spans (plist-get snapshot :buffer-spans))
(footprint
(or (plist-get snapshot :span-footprint-signature)
(with-current-buffer buffer
(ebox--spans-span-footprint-signature spans))))
(widths (plist-get footprint :line-pixel-widths))
((> (length widths) 0)))
(let* ((axis (ebox--flex-main-axis parent))
(slot-main (if (eq axis 'row)
(apply #'max widths)
(length widths)))
(declared (ebox--flex-box-main-constraint
style-node axis
(if (eq axis 'row) :width :height))))
(and declared (= slot-main declared)))))
(defun ebox--flex-item-footprint-keys-p (buffer node-id changed-keys)
"Return non-nil when CHANGED-KEYS can change NODE-ID's flex footprint.
Both axes count: main-axis keys change the item's slot width and
cross-axis keys change its row footprint, exactly the pair
`ebox--flex-slot-allocation-stable-p' refuses for slot reuse."
(and changed-keys
(when-let* ((parent-id (ebox--runtime-parent-id buffer node-id))
(parent (ebox--buffer-runtime-node buffer parent-id))
((eq (ebox--display-inner parent) 'flex))
(node (ebox--buffer-runtime-node buffer node-id))
(style-node (ebox-fragment-style-source-node node)))
(let ((axis (ebox--flex-main-axis parent)))
(cl-some (lambda (key)
(or (ebox--flex-main-footprint-key-p
key axis style-node)
(ebox--flex-main-footprint-key-p
key (ebox--flex-cross-axis axis) style-node)))
changed-keys)))))
(defun ebox--flex-child-for-source-node (flex-node node-id)
"Return FLEX-NODE's child whose source owns NODE-ID."
(cl-find-if
(lambda (child)
(equal (plist-get child :node-id) node-id))
(ebox-tree-layout-children flex-node)))
(defun ebox--flex-cross-axis (axis)
"Return the axis perpendicular to flex AXIS."
(if (eq axis 'row) 'column 'row))
(defun ebox--flex-content-allocation-stable-p
(buffer style-node item-props axis current-main)
"Return non-nil when content cannot change this flex allocation.
STYLE-NODE must have a definite main size used by the default auto basis, and
its new minimum must still fit CURRENT-MAIN."
(when (and (eq (plist-get item-props :flex-basis) 'auto)
(ebox--flex-main-definite-size-p style-node axis))
(let ((rendered
(if (and (eq axis 'row)
(ebox-style-no-soft-wrap-p
(ebox-get style-node :wrap-mode)))
(ebox--with-buffer-render-context buffer
(ebox--render-node style-node))
"")))
(<= (ebox--flex-min-main style-node rendered axis)
current-main))))
(defun ebox--flex-fixed-basis-content-allocation-stable-p
(style-node item-props axis)
"Return non-nil when content cannot affect a fixed-basis flex allocation.
A definite basis owns the item's base size. An explicit zero main-axis
minimum removes the content-minimum clamp, so grow/shrink allocation depends
only on the stable container and sibling flex inputs. Cross-axis compatibility
is still verified by the existing rendered slot-footprint proof."
(let* ((basis-value (plist-get item-props :flex-basis))
(basis (and (not (memq basis-value
'(nil auto min-content max-content
fit-content stretch contain viewport)))
(ebox--flex-main-value axis basis-value nil)))
(minimum (ebox-get style-node
(if (eq axis 'row) :min-width :min-height))))
(and (numberp basis) (>= basis 0)
(numberp minimum) (= minimum 0))))
(defun ebox--flex-item-fixed-basis-content-allocation-stable-p
(buffer node-id)
"Return whether NODE-ID has a content-independent fixed flex basis."
(when-let* ((node (ebox--buffer-runtime-node buffer node-id))
(parent-id (ebox--runtime-parent-id buffer node-id))
(parent (ebox--buffer-runtime-node buffer parent-id))
((eq (ebox--display-inner parent) 'flex))
(child (ebox--flex-child-for-source-node parent node-id))
(style-node (ebox-fragment-style-source-node node)))
(ebox--flex-fixed-basis-content-allocation-stable-p
style-node (ebox--flex-item-props child) (ebox--flex-main-axis parent))))
(defun ebox--flex-slot-allocation-stable-p
(buffer node child axis current-main changed-keys)
"Return non-nil when CHANGED-KEYS preserve NODE's flex allocation.
Unknown changes fail closed. A fixed outer size is insufficient for content
updates unless the auto flex basis stays fixed and the new minimum still fits
the already published slot."
(when-let* ((style-node (ebox-fragment-style-source-node node)))
(let* ((item-props (ebox--flex-item-props child))
(fixed-basis-p
(ebox--flex-fixed-basis-content-allocation-stable-p
style-node item-props axis)))
(and changed-keys
;; A fixed basis plus zero main minimum makes content independent
;; of main allocation. Admit cross-axis content changes to the
;; retained-slot render; stretch sizes the candidate to the old
;; cross slot, while non-stretch items remain natural and fail the
;; final footprint comparison if their block size changed.
(or fixed-basis-p
(cl-every
(lambda (key)
(and (not (ebox--flex-main-footprint-key-p
key axis style-node))
(not (ebox--flex-main-footprint-key-p
key (ebox--flex-cross-axis axis) style-node))))
changed-keys))
(or fixed-basis-p
(not (cl-some #'ebox--flex-content-flow-main-key-p
changed-keys))
(ebox--flex-content-allocation-stable-p
buffer style-node item-props axis current-main))))))
(defun ebox--flex-item-slot-sized-node
(buffer node-id snapshot &optional changed-keys)
"Return NODE-ID copied to its currently rendered flex slot.
SNAPSHOT supplies the already published parent-facing footprint. This keeps
incremental rendering in the same used-size context as the parent flex layout
instead of re-expanding the source node to its declared intrinsic size.
CHANGED-KEYS must prove that reusing the allocation is safe."
(when-let* ((node (ebox--buffer-runtime-node buffer node-id))
((eq (plist-get node :ebox-type) 'box))
(parent-id (ebox--runtime-parent-id buffer node-id))
(parent (ebox--buffer-runtime-node buffer parent-id))
((eq (ebox--display-inner parent) 'flex))
(child (ebox--flex-child-for-source-node parent node-id))
(spans (plist-get snapshot :buffer-spans))
(footprint
(or (plist-get snapshot :span-footprint-signature)
(with-current-buffer buffer
(ebox--spans-span-footprint-signature spans))))
(widths (plist-get footprint :line-pixel-widths))
((and widths (> (length widths) 0))))
(let* ((container-props
(ebox--flex-container-content-props
(plist-get parent :props)
(plist-get parent :raw-props)
(plist-get parent :box)))
(axis (ebox--flex-axis container-props))
(main (if (eq axis 'row)
(apply #'max widths)
(length widths)))
(cross (if (eq axis 'row)
(length widths)
(apply #'max widths)))
(item-props (ebox--flex-item-props child))
(item-align (plist-get item-props :align-self))
(align (if (memq item-align '(nil auto))
(plist-get container-props :align-items)
item-align))
(stretch (memq align '(stretch normal))))
(when (ebox--flex-slot-allocation-stable-p
buffer node child axis main changed-keys)
(ebox--flex-copy-node-for-size node axis main cross stretch)))))
(defun ebox--render-node-in-current-flex-slot
(buffer node-id snapshot &optional changed-keys)
"Render NODE-ID in BUFFER using its proven-stable flex slot, or return nil."
(when-let* ((node (ebox--buffer-runtime-node buffer node-id))
(sized-node
(ebox--flex-item-slot-sized-node
buffer node-id snapshot changed-keys)))
(prog1
(ebox--render-node sized-node)
;; Rendering the sized copy reuses the stable region id. Restore the
;; authoritative source object so later public updates never target the
;; temporary used-size copy.
(ebox--flex-recache-source-boxes node))))
(defun ebox--flex-item-slot-footprint-safe-p
(buffer node-id changed-keys)
"Return non-nil when NODE-ID's current render still fits its old flex slot."
(when-let* ((snapshot (ebox--ensure-layout-snapshot-details buffer node-id))
(spans (plist-get snapshot :buffer-spans))
(old-span-footprint
(plist-get snapshot :span-footprint-signature))
(old-external-footprint
(plist-get snapshot :external-footprint-signature))
(old-parent-slot
(plist-get snapshot :parent-slot-signature))
(node (ebox--buffer-runtime-node buffer node-id))
((not (ebox--node-visible-overflow-p node))))
(ebox--with-buffer-render-context buffer
(let* ((rendered
(or (ebox--render-node-in-current-flex-slot
buffer node-id snapshot changed-keys)
(ebox--render-node node)))
(role-owned-lines-p
(ebox--flex-item-fixed-basis-content-allocation-stable-p
buffer node-id))
(owned-rendered
(if role-owned-lines-p
(ebox-buffer--rendered-owned-lines
rendered (plist-get snapshot :region-ids) (length spans))
rendered))
(owner-set (ebox--region-id-set
(plist-get snapshot :region-ids)))
(slot-rendered
(and owned-rendered
(with-current-buffer buffer
(ebox-buffer--rendered-in-existing-slots
spans owned-rendered
(and role-owned-lines-p owner-set)))))
(final-rendered slot-rendered)
(new-span-footprint
(and final-rendered
(ebox--rendered-span-footprint-signature final-rendered)))
(new-external-footprint
(and new-span-footprint
(ebox--external-footprint-signature-from-span-footprint
new-span-footprint)))
(new-parent-slot
(and new-span-footprint
(ebox--project-parent-slot-signature
old-parent-slot new-span-footprint))))
(and final-rendered
(ebox--span-footprint-compatible-p
old-span-footprint new-span-footprint)
(ebox--external-footprint-compatible-p
old-external-footprint new-external-footprint)
(ebox--parent-slot-compatible-p
old-parent-slot new-parent-slot))))))
(defun ebox--cached-flex-item-slot-footprint-safe-p
(buffer node-id changed-keys)
"Return cached flex slot footprint safety for NODE-ID in BUFFER."
(if (not ebox--flex-slot-safety-cache)
(ebox--flex-item-slot-footprint-safe-p buffer node-id changed-keys)
(let* ((key (list buffer node-id changed-keys))
(missing (make-symbol "ebox-flex-slot-safety-missing"))
(cached (gethash key ebox--flex-slot-safety-cache missing)))
(if (not (eq cached missing))
cached
(puthash key
(ebox--flex-item-slot-footprint-safe-p
buffer node-id changed-keys)
ebox--flex-slot-safety-cache)))))
(defun ebox--flex-item-slot-safe-p
(buffer node-id changed-keys &optional own-change)
"Return non-nil when NODE-ID can keep its old flex item slot.
Footprint CHANGED-KEYS on either axis veto the slot when OWN-CHANGE
is non-nil. For a climbed ancestor (OWN-CHANGE nil) they are
tolerable only while the slot is anchored at the item's own declared
main size: a descendant's size-family change flows into the item's
content minimum, and an allocation decided by that minimum (or by
grow/shrink) legitimately changes with it, so a slot-preserving
publication would freeze stale sibling geometry."
(ignore own-change)
(and (ebox--direct-flex-parent-p buffer node-id)
(or (not (ebox--flex-item-footprint-keys-p
buffer node-id changed-keys))
;; A definite item whose published slot equals its declared main
;; size remains allocation-stable even when its own content changes.
;; The following slot-footprint proof still renders the candidate
;; in that size and rejects wrapping/overflow/parent-slot misses.
(ebox--flex-item-slot-at-declared-main-p buffer node-id)
(ebox--flex-item-fixed-basis-content-allocation-stable-p
buffer node-id))
(ebox--cached-flex-item-slot-footprint-safe-p
buffer node-id changed-keys)))
(defun ebox--contained-partial-flex-owner-p (buffer node-id)
"Return non-nil when NODE-ID is a definite flex stored in partial slots."
(when-let* ((node (ebox--buffer-runtime-node buffer node-id))
((eq (ebox--display-inner node) 'flex))
((ebox--definite-containment-formatting-context-p
buffer node-id))
(snapshot (ebox--ensure-layout-snapshot-spans buffer node-id))
(spans (plist-get snapshot :buffer-spans)))
(with-current-buffer buffer
(ebox-buffer--partial-line-slots-p spans))))
(defun ebox--dirty-node-is-owner-style-source-p
(buffer dirty-node-id owner-id)
"Return non-nil when DIRTY-NODE-ID carries OWNER-ID's outer style."
(when-let* ((owner (ebox--buffer-runtime-node buffer owner-id))
(style-source (ebox-fragment-style-source-node owner)))
(equal dirty-node-id (plist-get style-source :node-id))))
(defun ebox--first-geometry-owner-candidate
(buffer node-id dirty-kind &optional changed-keys impact-vector)
"Return the first structurally possible patch owner for NODE-ID.
Geometry changes inside layout containers must promote to the container before
running expensive span and snapshot checks."
(let ((candidate node-id)
(dirty-node-id node-id))
(while (and (eq dirty-kind 'geometry)
(ebox--impact-vector-requires-parent-promotion-p
impact-vector)
(not (and (not (equal candidate dirty-node-id))
(ebox--contained-partial-flex-owner-p
buffer candidate)
(not
(ebox--dirty-node-is-owner-style-source-p
buffer dirty-node-id candidate))))
(ebox--geometry-affects-parent-layout-p buffer candidate)
(not (ebox--flex-item-slot-safe-p
buffer candidate changed-keys
(equal candidate dirty-node-id))))
(setq candidate (ebox--runtime-parent-id buffer candidate)))
candidate))
(defun ebox--partial-slot-owner-p (buffer node-id)
"Return non-nil when NODE-ID owns replaceable partial row/flex slots."
(when-let* ((snapshot (ebox--ensure-layout-snapshot-spans buffer node-id))
(spans (plist-get snapshot :buffer-spans)))
(with-current-buffer buffer
(and (ebox--direct-flex-parent-p buffer node-id)
(cl-some (lambda (span)
(not (ebox--span-whole-line-p span)))
spans)))))
(defun ebox--patchable-owner-p
(buffer node-id &optional dirty-kind allow-owned-overflow-coverage)
"Return non-nil when NODE-ID can be safely range patched in BUFFER."
(when-let* ((node (ebox--buffer-runtime-node buffer node-id))
(snapshot (ebox--ensure-layout-snapshot-spans buffer node-id))
(spans (plist-get snapshot :buffer-spans)))
(with-current-buffer buffer
(let* ((ebox--render-source-index
(plist-get (ebox--buffer-render-state buffer) :source-index))
(partial-slot (ebox--partial-slot-owner-p buffer node-id))
(coverage-flex
(and allow-owned-overflow-coverage
(eq (ebox--display-inner node) 'flex)))
(contained-flex
(ebox--contained-partial-flex-owner-p buffer node-id))
(local-flex (or coverage-flex contained-flex)))
(and (or (eq dirty-kind 'span)
(ebox--line-spans-cover-whole-lines-p spans)
(and (eq dirty-kind 'geometry) partial-slot)
(and (memq dirty-kind '(geometry structure))
local-flex))
(not (and (eq dirty-kind 'geometry)
(ebox--geometry-affects-parent-layout-p buffer node-id)
(not partial-slot)
(not local-flex)))
(or partial-slot
local-flex
(not (ebox--node-visible-overflow-p node))))))))
(defun ebox--span-patchable-owner-p (buffer node-id)
"Return non-nil when NODE-ID can attempt verified span patching."
(when-let* ((node (ebox--buffer-runtime-node buffer node-id)))
(and ;; Planning only needs to reject an explicitly visible overflow
;; owner. Content-dependent overflow remains part of the final
;; span/role validator; formatting it here for every ancestor made
;; owner selection pay the expensive proof twice.
(not (eq (ebox-get node :overflow) 'visible))
(or
;; Allocation shape is coordinate-independent and survives a
;; detail-generation bump. The later owner/surface proof still
;; resolves exact current spans before publication.
(when-let* ((snapshot (ebox--layout-snapshot buffer node-id)))
(plist-get snapshot :allocation-shape))
(when-let* ((snapshot
(ebox--ensure-layout-snapshot-spans buffer node-id))
(spans (plist-get snapshot :buffer-spans)))
spans)))))
(defun ebox--promote-to-patchable-owner
(buffer node-id &optional dirty-kind changed-keys impact-vector)
"Return the smallest patchable owner id for NODE-ID in BUFFER."
(let ((candidate (ebox--first-geometry-owner-candidate
buffer node-id dirty-kind changed-keys
impact-vector))
owner)
(while (and candidate (not owner))
(if (ebox--cached-patchable-owner-p buffer candidate dirty-kind)
(setq owner candidate)
(setq candidate (ebox--runtime-parent-id buffer candidate))))
owner))
(defun ebox--promote-to-span-patchable-owner
(buffer node-id &optional dirty-kind changed-keys)
"Return the smallest structural owner worth one verified span-patch attempt."
(let ((candidate node-id)
(root-id (ebox--buffer-root-node-id buffer))
owner)
(ignore dirty-kind)
(while (and candidate (not owner))
(cond
((equal candidate root-id)
(setq candidate nil))
((and (ebox--span-patchable-owner-p buffer candidate)
(let ((parent-id (ebox--runtime-parent-id buffer candidate)))
(or (not (ebox--geometry-affects-parent-layout-p
buffer candidate))
(not parent-id)
(and (not (equal candidate node-id))
(ebox--inline-size-change-p changed-keys)
(not
(ebox--dirty-node-is-owner-style-source-p
buffer node-id candidate))
(ebox--definite-containment-formatting-context-p
buffer candidate))
(ebox--flex-item-slot-safe-p
buffer candidate changed-keys
(equal candidate node-id)))))
(setq owner candidate))
(t
(setq candidate (ebox--runtime-parent-id buffer candidate)))))
owner))
(defun ebox--block-size-change-p (changed-keys)
"Return non-nil when CHANGED-KEYS contains only block-size constraints."
(and changed-keys
(cl-every (lambda (key)
(memq key '(:height :min-height :max-height)))
changed-keys)))
(defun ebox--block-size-containment-owner-id (buffer node-id)
"Return NODE-ID's nearest strict fixed partial-flex containment owner."
(let ((candidate (ebox--runtime-parent-id buffer node-id))
(root-id (ebox--buffer-root-node-id buffer))
owner)
(while (and candidate (not owner))
(cond
((equal candidate root-id)
(setq candidate nil))
((and (ebox--contained-partial-flex-owner-p buffer candidate)
(ebox--span-patchable-owner-p buffer candidate))
(setq owner candidate))
(t
(setq candidate (ebox--runtime-parent-id buffer candidate)))))
owner))
(defun ebox--runtime-child-under-ancestor (buffer ancestor-id node-id)
"Return the direct child id under ANCESTOR-ID on NODE-ID's runtime path."
(let ((child-id node-id)
(parent-id (ebox--runtime-parent-id buffer node-id))
found)
(while (and parent-id (not found))
(if (equal parent-id ancestor-id)
(setq found child-id)
(setq child-id parent-id
parent-id (ebox--runtime-parent-id buffer parent-id))))
found))
(defun ebox--span-patch-owner-id-for-dirty-entry (entry buffer)
"Return a local span patch owner for ENTRY in BUFFER, or nil."
(let* ((dirty-kind (plist-get entry :dirty-kind))
(source-node-id (plist-get entry :node-id))
(node-id (ebox--dirty-entry-geometry-owner-id buffer entry))
(source-parent-id
(and source-node-id
(ebox--runtime-parent-id buffer source-node-id)))
(source-parent
(and source-parent-id
(ebox--buffer-runtime-node buffer source-parent-id)))
(source-parent-kind
(and source-parent (ebox-tree-display-inner source-parent)))
(fast-grid-evidence
(and ebox-incremental--candidate-state-for-slot-proof
(eq dirty-kind 'geometry)
(memq :content (plist-get entry :changed-keys))
(ebox-incremental--fast-grid-slot-evidence
buffer entry
ebox-incremental--candidate-state-for-slot-proof)))
(impact-vector (or (plist-get entry :impact-vector)
(and (plist-get entry :changed-keys)
(ebox--dirty-entry-impact-vector
buffer entry)))))
(unless (ebox--impact-vector-requires-conservative-owner-p impact-vector)
(cond
(fast-grid-evidence
node-id)
((and ebox-incremental--candidate-state-for-slot-proof
(eq dirty-kind 'geometry)
(memq :content (plist-get entry :changed-keys))
;; The one-line source-slot proof only accepts Row/Column
;; parents. Reject Flow/Flex/Grid here before it materializes
;; snapshots and walks an ancestor certificate that its own
;; final predicate would discard.
(memq source-parent-kind '(row column))
(ebox-incremental--content-slot-stable-p
buffer entry
ebox-incremental--candidate-state-for-slot-proof))
source-node-id)
((and (ebox--dirty-entry-span-patch-candidate-p buffer entry)
(not (ebox--impact-vector-requires-parent-promotion-p
impact-vector))
(not (cl-some
(lambda (key)
(memq key ebox--span-patch-border-box-block-keys))
(plist-get entry :changed-keys)))
(ebox--cached-patchable-owner-p buffer node-id 'span)
(or (not (ebox--direct-flex-parent-p buffer node-id))
(ebox--flex-item-slot-safe-p
buffer node-id (plist-get entry :changed-keys) t)
(ebox--flex-item-fixed-basis-content-allocation-stable-p
buffer node-id)
(and ebox-incremental--candidate-state-for-slot-proof
(ebox-incremental--allocated-parent-slot-stable-p
buffer entry
ebox-incremental--candidate-state-for-slot-proof))))
node-id)
((and (eq dirty-kind 'geometry)
(plist-get entry :changed-keys))
(or (and (ebox--block-size-change-p
(plist-get entry :changed-keys))
(ebox--block-size-containment-owner-id buffer node-id))
(ebox--promote-to-span-patchable-owner
buffer node-id dirty-kind
(plist-get entry :changed-keys))))))))
(defun ebox--make-patch-op-merge-index ()
"Return fresh mutable state for indexed patch-op antichain merging."
(list :owner-table (make-hash-table :test 'equal)
:ancestor-table (make-hash-table :test 'equal)
:serial 0))
(defun ebox--patch-op-merge-index-insert (buffer index entry)
"Register ENTRY (a vector [OP SERIAL ANCESTOR-SET]) into INDEX."
(puthash (plist-get (aref entry 0) :owner-id)
entry (plist-get index :owner-table))
(let ((ancestor-table (plist-get index :ancestor-table)))
(maphash (lambda (ancestor-id _present)
(push entry (gethash ancestor-id ancestor-table)))
(aref entry 2)))
(ignore buffer))
(defun ebox--patch-op-merge-index-remove (index entry)
"Remove ENTRY from INDEX."
(remhash (plist-get (aref entry 0) :owner-id)
(plist-get index :owner-table))
(let ((ancestor-table (plist-get index :ancestor-table)))
(maphash (lambda (ancestor-id _present)
(puthash ancestor-id
(delq entry (gethash ancestor-id ancestor-table))
ancestor-table))
(aref entry 2))))
(defun ebox--patch-op-merge-index-add (buffer index candidate)
"Merge CANDIDATE into indexed antichain INDEX for BUFFER.
Preserves `ebox--merge-patch-op-into-set' semantics exactly: a
same-owner op merges by strength first, then the earliest-inserted
op at an ancestor with span strength or better absorbs the candidate,
otherwise the candidate absorbs every entry below it (in insertion
order) and joins the set. Ancestor relations resolve through the
per-node ancestor sets, so each insertion costs one walk plus
hash lookups instead of a scan of the whole set."
(let* ((owner-table (plist-get index :owner-table))
(owner-id (plist-get candidate :owner-id))
(same-owner (gethash owner-id owner-table)))
(when same-owner
(setq candidate (ebox--merge-same-owner-patch-ops
(aref same-owner 0) candidate))
(ebox--patch-op-merge-index-remove index same-owner))
(let* ((ancestors (ebox--runtime-ancestor-id-set buffer owner-id))
(dominator nil))
(maphash
(lambda (ancestor-id _present)
(when-let* ((entry (gethash ancestor-id owner-table)))
(when (and (>= (ebox--patch-operation-strength
(plist-get (aref entry 0) :op))
(ebox--patch-operation-strength 'span-patch))
(or (null dominator)
(< (aref entry 1) (aref dominator 1))))
(setq dominator entry))))
ancestors)
(if dominator
(aset dominator 0
(ebox--patch-op-merge-dirty (aref dominator 0) candidate))
(when (>= (ebox--patch-operation-strength
(plist-get candidate :op))
(ebox--patch-operation-strength 'span-patch))
(dolist (entry (sort (copy-sequence
(gethash owner-id
(plist-get index :ancestor-table)))
(lambda (a b) (< (aref a 1) (aref b 1)))))
(setq candidate
(ebox--patch-op-merge-dirty candidate (aref entry 0)))
(ebox--patch-op-merge-index-remove index entry)))
(let ((entry (vector candidate
(plist-get index :serial)
ancestors)))
(plist-put index :serial (1+ (plist-get index :serial)))
(ebox--patch-op-merge-index-insert buffer index entry))))))
(defun ebox--patch-op-merge-index-result (index)
"Return INDEX's surviving ops in insertion order."
(let (entries)
(maphash (lambda (_owner-id entry) (push entry entries))
(plist-get index :owner-table))
(mapcar (lambda (entry) (aref entry 0))
(sort entries (lambda (a b) (< (aref a 1) (aref b 1)))))))
(defun ebox--dedupe-patch-owners (buffer ops)
"Merge same-owner OPS and remove structurally covered descendants in BUFFER."
(let ((index (ebox--make-patch-op-merge-index)))
(dolist (op ops)
(ebox--patch-op-merge-index-add buffer index op))
(ebox--patch-op-merge-index-result index)))
(defun ebox--next-patchable-owner-ancestor
(buffer owner-id dirty-kind &optional require-owned-overflow-coverage)
"Return the nearest non-root patchable ancestor above OWNER-ID.
When REQUIRE-OWNED-OVERFLOW-COVERAGE is non-nil, skip ancestors whose wrapper
cannot cover an old unowned visible-overflow suffix."
(let ((candidate (ebox--runtime-parent-id buffer owner-id))
(root-id (ebox--buffer-root-node-id buffer))
owner)
(while (and candidate (not owner))
(cond
((equal candidate root-id)
(setq candidate nil))
((and (or (not require-owned-overflow-coverage)
(ebox--owned-overflow-coverage-wrapper-node-p
buffer candidate))
(ebox--cached-patchable-owner-p
buffer candidate dirty-kind require-owned-overflow-coverage))
(setq owner candidate))
(t
(setq candidate (ebox--runtime-parent-id buffer candidate)))))
owner))
(defun ebox--patch-results-strategy (results)
"Return aggregate update strategy for executed patch RESULTS."
(cond
((cl-some (lambda (result)
(eq (plist-get result :op) 'native-frame))
results)
'native-frame)
((cl-some (lambda (result)
(eq (plist-get result :op) 'owner-rerender))
results)
'owner-rerender)
((cl-some (lambda (result)
(eq (plist-get result :op) 'span-patch))
results)
'span-patch)
((cl-some (lambda (result)
(eq (plist-get result :op) 'child-splice))
results)
'child-splice)
((cl-some (lambda (result)
(eq (plist-get result :op) 'child-reorder))
results)
'child-reorder)
((cl-every (lambda (result)
(eq (plist-get result :op) 'paint-patch))
results)
'paint-patch)
(t 'owner-rerender)))
(defun ebox--viewport-dependent-size-value-p (value &optional nil-dependent)
"Return non-nil when horizontal size VALUE depends on viewport context.
When NIL-DEPENDENT is non-nil, nil is treated as viewport-dependent auto width."
(or (and nil-dependent (null value))
(and nil-dependent (eq value 'auto))
(memq value '(stretch contain fit-content viewport))
(and (consp value)
(or (eq (car value) 'viewport)
(and (eq (car value) 'fit-content)
(ebox--viewport-dependent-size-value-p (cadr value)))))))
(defun ebox--viewport-dependent-height-value-p (value)
"Return non-nil when vertical size VALUE depends on viewport height."
(or (eq value 'viewport-height)
(and (consp value)
(or (eq (car value) 'viewport-height)
(and (memq (car value) '(+ -))
(cl-some #'ebox--viewport-dependent-height-value-p
(cdr value)))))))
(defun ebox--viewport-dependent-height-props-p (props)
"Return non-nil when PROPS has a viewport-height size constraint."
(cl-some
(lambda (property)
(ebox--viewport-dependent-height-value-p
(plist-get props property)))
'(:height :min-height :max-height)))
(defun ebox--node-direct-viewport-height-dependent-p (node)
"Return non-nil when NODE's own vertical size uses viewport height."
(when (and (listp node)
(not (stringp node)))
(pcase (plist-get node :ebox-type)
('box
(ebox--viewport-dependent-height-props-p node))
('flex
(or (ebox--viewport-dependent-height-props-p
(plist-get node :props))
(ebox--viewport-dependent-height-props-p
(plist-get node :raw-props)))))))
(defun ebox--flex-participation-viewport-dependent-p (node)
"Return non-nil when NODE's flex item metadata depends on viewport context."
(when-let* ((participation (ebox--flex-participation-props node)))
(ebox--viewport-dependent-size-value-p
(plist-get participation :flex-basis))))
(defun ebox--node-direct-viewport-width-dependent-p (node)
"Return non-nil when NODE's own layout depends on viewport width."
(when (listp node)
(and (not (eq (plist-get node :ebox-kind) 'text))
(or (ebox--flex-participation-viewport-dependent-p node)
(cond
((eq (plist-get node :ebox-kind) 'box)
(or (ebox--viewport-dependent-size-value-p
(plist-get node :width) t)
(ebox--viewport-dependent-size-value-p
(plist-get node :min-width))
(ebox--viewport-dependent-size-value-p
(plist-get node :max-width))))
((eq (ebox--display-inner node) 'flex)
(ebox--viewport-dependent-size-value-p
(plist-get (plist-get node :props) :width) t))
((eq (ebox--display-inner node) 'flow)
(or (ebox--viewport-dependent-size-value-p
(plist-get node :width) t)
(ebox--viewport-dependent-size-value-p
(plist-get node :min-width))
(ebox--viewport-dependent-size-value-p
(plist-get node :max-width)))))))))
(defun ebox--node-direct-viewport-dependent-p (node)
"Return non-nil when NODE's own layout depends on viewport context."
(or (ebox--node-direct-viewport-width-dependent-p node)
(ebox--node-direct-viewport-height-dependent-p node)))
(defun ebox--viewport-height-dependent-subtree-p (node)
"Return non-nil when NODE or a descendant depends on viewport height."
(when (and (listp node)
(not (stringp node)))
(cl-labels
((compute ()
(or (ebox--node-direct-viewport-height-dependent-p node)
(cl-some #'ebox--viewport-height-dependent-subtree-p
(ebox--node-children node)))))
(if (not ebox--viewport-height-dependent-subtree-cache)
(compute)
(let* ((missing (make-symbol "ebox-viewport-height-subtree-missing"))
(cached
(gethash node ebox--viewport-height-dependent-subtree-cache
missing)))
(if (not (eq cached missing))
cached
(puthash node (compute)
ebox--viewport-height-dependent-subtree-cache)))))))
(defun ebox--viewport-dependent-subtree-p (node)
"Return non-nil when NODE or any descendant depends on viewport width."
(when (and (listp node)
(not (stringp node)))
(if (not ebox--viewport-dependent-subtree-cache)
(or (ebox--node-direct-viewport-width-dependent-p node)
(cl-some #'ebox--viewport-dependent-subtree-p
(ebox--node-children node)))
(let* ((missing (make-symbol "ebox-viewport-subtree-missing"))
(cached (gethash node ebox--viewport-dependent-subtree-cache
missing)))
(if (not (eq cached missing))
cached
(puthash
node
(or (ebox--node-direct-viewport-width-dependent-p node)
(cl-some #'ebox--viewport-dependent-subtree-p
(ebox--node-children node)))
ebox--viewport-dependent-subtree-cache))))))
(defun ebox--viewport-dependent-node-id-axes (node)
"Return (WIDTH-IDS . HEIGHT-IDS) below NODE.
Definite width contains descendant width ids, but not height ids."
(when (and (listp node)
(not (stringp node)))
(cl-labels
((collect ()
(let* ((width-direct
(ebox--node-direct-viewport-width-dependent-p node))
(vertical
(ebox--node-direct-viewport-height-dependent-p node))
(contained
(ebox--render-cache-contained-viewport-node-p node))
(children (ebox--node-children node))
(relevant-children
(if contained
(cl-remove-if-not
#'ebox--viewport-height-dependent-subtree-p
children)
children))
(child-axes
(mapcar #'ebox--viewport-dependent-node-id-axes
relevant-children))
(node-id (and (or width-direct vertical)
(ebox--ensure-node-id node))))
(cons
(append
(when width-direct
(list node-id))
(unless contained
(apply #'append (mapcar #'car child-axes))))
(append
(when vertical
(list node-id))
(apply #'append (mapcar #'cdr child-axes)))))))
(if (not ebox--viewport-dependent-node-ids-cache)
(collect)
(let* ((missing (make-symbol "ebox-viewport-dependent-missing"))
(cached (gethash node ebox--viewport-dependent-node-ids-cache
missing)))
(if (not (eq cached missing))
cached
(puthash node (collect)
ebox--viewport-dependent-node-ids-cache)))))))
(defun ebox--viewport-dependent-node-ids (node)
"Return runtime node ids under NODE that depend on viewport context."
(when-let* ((axes (ebox--viewport-dependent-node-id-axes node)))
(delete-dups (copy-sequence (append (car axes) (cdr axes))))))
(defun ebox--viewport-dirty-set (root)
"Return viewport dirty-set entries for ROOT."
(mapcar (lambda (node-id)
(ebox--dirty-entry node-id 'geometry))
(delete-dups
(copy-sequence (ebox--viewport-dependent-node-ids root)))))
(defun ebox--viewport-constraint-change (buffer &optional axes)
"Return the implicit viewport constraint change for BUFFER.
AXES may be `width', `height', `both', or `none'. Nil means both axes for
legacy callers."
(when-let* ((root (ebox--buffer-root-node buffer)))
(let* ((node-ids
(pcase axes
('width
(car (ebox--buffer-viewport-dependent-node-id-axes buffer)))
('height
(cdr (ebox--buffer-viewport-dependent-node-id-axes buffer)))
('none nil)
(_
(ebox--buffer-viewport-dependent-node-ids buffer))))
(node-ids (delete-dups (copy-sequence node-ids)))
(root-node-id (ebox--ensure-node-id root))
(coalesced
(>= (length node-ids) ebox--owner-coalesce-min-count))
(dirty-set
(if coalesced
(list (ebox--dirty-entry
root-node-id 'geometry :node-ids node-ids))
(mapcar (lambda (node-id)
(ebox--dirty-entry node-id 'geometry))
node-ids))))
(ebox--constraint-change
'viewport 'viewport 'viewport
root-node-id dirty-set
:coalesced-dirty-count (and coalesced (length node-ids))))))
(defun ebox--region-update-dirty-kind (changed-keys)
"Return the dirty kind for CHANGED-KEYS from `ebox-region-update'."
(if (cl-every (lambda (key)
(memq key ebox--paint-style-signature-keys))
changed-keys)
'paint
'geometry))
(defun ebox--region-constraint-change
(buffer region-id dirty-kind &optional changed-keys old-style-values)
"Return the explicit region constraint change for REGION-ID in BUFFER."
(when-let* ((root (ebox--buffer-root-node buffer))
(dirty-node
(ebox--buffer-region-render-owner-node buffer region-id))
(dirty-node-id (ebox--ensure-node-id dirty-node)))
(let ((dirty-entry
(ebox--dirty-entry dirty-node-id dirty-kind
:region-id region-id
:changed-keys changed-keys
:old-style-values old-style-values
:region-ids
(and (eq dirty-kind 'paint)
(ebox--node-all-region-ids dirty-node)))))
(ebox--constraint-change
'region dirty-node-id (plist-get dirty-node :ebox-type)
(ebox--ensure-node-id root)
(list (ebox--dirty-entry-with-impact-vector buffer dirty-entry))
:region-id region-id))))
(defun ebox--update-report (region-id strategy &rest props)
"Build an update report for REGION-ID using STRATEGY and PROPS."
(append (list :region-id region-id
:strategy strategy
:full-rerender (eq strategy 'full-rerender))
props
(when ebox-cache-report-buffer
(ebox-cache-report ebox-cache-report-buffer))))
(defun ebox-incremental--batch-state (buffer)
"Return explicit batch state for BUFFER."
(gethash buffer ebox-incremental--batch-table))
(defun ebox-incremental-batching-p (buffer)
"Return non-nil when BUFFER is inside an explicit incremental batch."
(and (buffer-live-p buffer)
(plist-get (ebox-incremental--batch-state buffer) :active)))
(defun ebox-incremental-begin-batch (buffer)
"Begin an explicit incremental update batch for BUFFER."
(unless (buffer-live-p buffer)
(error "ebox-incremental: buffer is not live: %S" buffer))
(let ((state (list :active t
:pending nil
:flush-count 0
:surface-candidate-root nil
:surface-source-base-index nil
:surface-publisher nil)))
(puthash buffer state ebox-incremental--batch-table)
state))
(defun ebox-incremental-pending-count (buffer)
"Return the number of pending explicit batch changes for BUFFER."
(length (plist-get (ebox-incremental--batch-state buffer) :pending)))
(defun ebox-incremental-surface-batch-root (buffer)
"Return BUFFER's unpublished surface batch root, or nil."
(let ((state (ebox-incremental--batch-state buffer)))
(and (plist-get state :active)
(plist-get state :surface-candidate-root))))
(defun ebox-incremental-surface-batch-source-index (buffer)
"Return BUFFER's unpublished batch source index, or nil."
(let ((state (ebox-incremental--batch-state buffer)))
(and (plist-get state :active)
(plist-get state :surface-source-base-index))))
(defun ebox-incremental-record-surface-region-change
(buffer candidate-root region-id handle dirty-kind changed-keys
old-style-values publisher)
"Record one surface-scoped change for BUFFER's explicit batch.
CANDIDATE-ROOT is the accumulated unpublished tree. REGION-ID and HANDLE
identify the logical target, DIRTY-KIND, CHANGED-KEYS, and OLD-STYLE-VALUES
describe the semantic change, and PUBLISHER commits the complete batch once
during `ebox-incremental-flush'."
(ebox-incremental-record-surface-region-input-change
buffer candidate-root nil region-id handle dirty-kind changed-keys
old-style-values publisher))
(defun ebox-incremental-record-surface-region-input-change
(buffer candidate-root source-base-index region-id handle dirty-kind
changed-keys old-style-values publisher)
"Record one batched region change and its candidate SOURCE-BASE-INDEX."
(when (ebox-incremental-batching-p buffer)
(let ((state (ebox-incremental--batch-state buffer)))
(plist-put state :surface-candidate-root candidate-root)
(when source-base-index
(plist-put state :surface-source-base-index source-base-index))
(plist-put state :surface-publisher publisher)
(plist-put state :pending
(append
(plist-get state :pending)
(list (list :region-id region-id
:region-handle handle
:dirty-kind dirty-kind
:changed-keys changed-keys
:old-style-values old-style-values))))
t)))
(defun ebox-incremental--merge-old-style-values (old-values additions)
"Merge ADDITIONS into OLD-VALUES while preserving each first old value."
(let ((merged (copy-sequence old-values))
(additions (copy-sequence additions)))
(while additions
(let ((key (pop additions))
(value (pop additions)))
(unless (plist-member merged key)
(setq merged (append merged (list key value))))))
merged))
(defun ebox-incremental--batch-dirty-set (buffer pending)
"Return a dirty set for BUFFER from PENDING batch entries."
(let ((seen (make-hash-table :test 'equal))
dirty-set)
(dolist (entry pending)
(when-let* ((region-id (plist-get entry :region-id))
(dirty-kind (plist-get entry :dirty-kind))
(dirty-node
(ebox--buffer-region-render-owner-node buffer region-id))
(node-id (ebox--ensure-node-id dirty-node)))
(let* ((key (list node-id dirty-kind))
(changed-keys (plist-get entry :changed-keys))
(existing (gethash key seen)))
(if existing
(progn
(plist-put
existing :changed-keys
(delete-dups
(append (copy-sequence
(or (plist-get existing :changed-keys) nil))
(copy-sequence changed-keys))))
(plist-put
existing :old-style-values
(ebox-incremental--merge-old-style-values
(plist-get existing :old-style-values)
(plist-get entry :old-style-values)))
(ebox--dirty-entry-with-impact-vector buffer existing))
(let ((dirty-entry
(ebox--dirty-entry
node-id dirty-kind
:region-id region-id
:changed-keys changed-keys
:old-style-values (plist-get entry :old-style-values)
:region-ids
(and (eq dirty-kind 'paint)
(ebox--node-all-region-ids dirty-node)))))
(setq dirty-entry
(ebox--dirty-entry-with-impact-vector buffer dirty-entry))
(puthash key dirty-entry seen)
(push dirty-entry dirty-set))))))
(nreverse dirty-set)))
(defun ebox-incremental--batch-change (buffer pending)
"Return a normalized batch constraint change for BUFFER and PENDING."
(when-let* ((root (ebox--buffer-root-node buffer)))
(ebox--constraint-change
'batch 'batch 'batch
(ebox--ensure-node-id root)
(ebox-incremental--batch-dirty-set buffer pending))))
(defun ebox-incremental--flush (buffer)
"Flush BUFFER's explicit incremental update batch without observing it."
(unless (buffer-live-p buffer)
(error "ebox-incremental: buffer is not live: %S" buffer))
(let* ((state (ebox-incremental--batch-state buffer))
(pending (plist-get state :pending))
(surface-publisher (plist-get state :surface-publisher)))
(unless state
(error "ebox-incremental: no active batch for buffer: %S" buffer))
(plist-put state :active nil)
(plist-put state :pending nil)
(plist-put state :flush-count
(1+ (or (plist-get state :flush-count) 0)))
(if (null pending)
(ebox--set-buffer-update-report
buffer
(ebox--update-report nil 'no-op
:dirty-count 0
:patch-count 0
:patch-ops nil
:flush-count 1))
(ebox--invalidate-buffer-viewport-dependencies buffer)
(unless surface-publisher
(error "Ebox batch has no TP surface publisher"))
(let (report)
(unwind-protect
(let ((ebox-incremental--source-base-index
(plist-get state :surface-source-base-index)))
(setq report
(funcall
surface-publisher buffer
(plist-get state :surface-candidate-root) pending)))
;; A quit or render error during execution must leave the
;; batch active with its recorded changes still pending so a
;; later retry cannot silently publish a no-op. If execution
;; returned a report, including an explicit no-op report, the
;; flush completed and the batch should close normally.
(when (null report)
(plist-put state :active t)
(plist-put state :pending pending)
(plist-put state :flush-count
(1- (plist-get state :flush-count)))))
(setq report
(append report
(list :flush-count 1
:batched-count (length pending))))
(ebox--set-buffer-update-report buffer report)
(when (and report
(not (eq (plist-get report :strategy) 'no-op)))
(unless (plist-get report :runtime-published)
(ebox--bump-buffer-runtime-revision buffer))
(run-hook-with-args
'ebox-incremental--after-successful-batch-flush-hook
buffer pending report))
(ebox--buffer-update-report buffer)))))
;;; Declarative Root Commit
(defun ebox-incremental-candidate-begin (buffer)
"Return a logical one-shot candidate based on BUFFER's current runtime."
(unless (buffer-live-p buffer)
(error "Ebox candidate requires a live buffer: %S" buffer))
(let ((state (ebox--buffer-render-state buffer)))
(unless state
(error "Ebox buffer has no rendered runtime: %S" buffer))
(ebox-incremental--make-candidate
:buffer buffer
:base-state state
:base-root (plist-get state :root-node)
:base-revision (or (plist-get state :runtime-revision) 0)
:base-buffer-tick
(with-current-buffer buffer (buffer-modified-tick))
:replacements nil
:paint-patches nil
:range-replacements nil
:range-metrics nil
:sealed-p nil)))
(defun ebox-incremental-candidate-replace-range-ref
(candidate ref next-items &optional reuse-map retain-item-identities-p)
"Record a Range REF replacement using proven slot REUSE-MAP."
(unless (ebox-candidate-p candidate)
(error "Ebox Range replacement requires an Ebox candidate"))
(when (ebox-candidate--sealed-p candidate)
(error "Ebox candidate is already sealed"))
(unless (proper-list-p next-items)
(signal 'wrong-type-argument (list 'proper-list-p next-items)))
(unless (proper-list-p reuse-map)
(signal 'wrong-type-argument (list 'proper-list-p reuse-map)))
(let* ((base-state (ebox-candidate--base-state candidate))
(record (gethash ref (plist-get base-state :range-ref-table))))
(unless record
(error "Ebox candidate Range ref does not exist in its base: %S" ref))
(let* ((retain-item-identities-p
(and retain-item-identities-p
(ebox-incremental--candidate-range-items-retainable-p
candidate next-items ebox-incremental--source-base-index
reuse-map)))
(parent (gethash (plist-get record :parent-node-id)
(plist-get base-state :node-table)))
(sequence (and parent (plist-get parent :ebox-child-sequence)))
(segment (and sequence (ebox-child-range--lookup-ref sequence ref)))
(old-items (and segment
(append (ebox-child-range--segment-payload segment)
nil)))
(aligned-p (= (length old-items) (length next-items)))
(reuse-by-new (make-hash-table :test #'eql))
(seen-old (make-hash-table :test #'eql))
(missing (make-symbol "ebox-range-reuse-missing"))
owned)
(when (and (eq (plist-get parent :ebox-kind) 'box)
(not (cl-every
(lambda (item)
(memq (plist-get item :ebox-kind) '(text box)))
next-items)))
(error "Canonical Ebox Box Range accepts only Text/Box nodes: %S"
next-items))
(dolist (entry reuse-map)
(let ((new-index (car-safe entry))
(old-index (cdr-safe entry)))
(unless (and (integerp new-index) (>= new-index 0)
(< new-index (length next-items))
(integerp old-index) (>= old-index 0)
(< old-index (length old-items))
(eq (gethash new-index reuse-by-new missing) missing)
(not (gethash old-index seen-old)))
(error "Ebox Range reuse map is invalid: %S" entry))
(puthash new-index (nth old-index old-items) reuse-by-new)
(puthash old-index t seen-old)))
(cl-labels
((validate-new
(item)
(unless (and (listp item) (not (stringp item)))
(error "Ebox child Range item must be a declarative node"))
(cl-labels
((reject-range
(node)
(when (ebox-child-range--descriptor-p node)
(error "Range replacement payload cannot introduce a Range"))
(when (and (listp node) (not (stringp node)))
(dolist (child (ebox-tree--children-raw node))
(reject-range child)))))
(reject-range item))
(let ((copy
(if retain-item-identities-p
item
(ebox-tree-clear-runtime-identities
(ebox-tree-copy-node-structure item)))))
(unless retain-item-identities-p
(ebox-tree-source-index
copy nil nil ebox-incremental--source-base-index t))
copy)))
(cl-loop
for item in next-items
for index from 0
for reused = (gethash index reuse-by-new missing)
for old = (if (eq reused missing)
(and aligned-p (nth index old-items))
reused)
do (push (if (or (not (eq reused missing))
(and aligned-p (eq item old)))
old
(validate-new item))
owned)))
(setq owned (nreverse owned))
(let ((source-indexes (make-hash-table :test #'eq))
(base-source-index (plist-get base-state :source-index)))
(dolist (item owned)
(let ((handle (ebox-tree-node-source-handle item)))
(puthash
handle
(if (and handle
(ebox-source-index-handle-member-p
base-source-index handle))
base-source-index
(if (and ebox-incremental--source-base-index
(ebox-source-index-handle-member-p
ebox-incremental--source-base-index handle))
ebox-incremental--source-base-index
(ebox-tree-source-index item)))
source-indexes)))
(setf (ebox-candidate--range-replacements candidate)
(append
(cl-remove
ref (ebox-candidate--range-replacements candidate)
:key #'ebox-incremental--candidate-range-replacement-ref
:test #'equal)
(list
(ebox-incremental--make-candidate-range-replacement
:ref ref :items owned :reuse-map (copy-tree reuse-map)
:source-indexes source-indexes
:retain-item-identities-p retain-item-identities-p))))))
candidate))
(defun ebox-incremental--candidate-ancestor-p
(candidate ancestor-id descendant-id)
"Return non-nil when ANCESTOR-ID contains DESCENDANT-ID in CANDIDATE's base."
(let* ((state (ebox-candidate--base-state candidate))
(parent-table (plist-get state :parent-table))
(current (gethash descendant-id parent-table))
found)
(while (and current (not found))
(if (equal current ancestor-id)
(setq found t)
(setq current (gethash current parent-table))))
found))
(defun ebox-incremental-candidate-replace
(candidate node-id next-subtree
&optional old-semantic-key new-semantic-key)
"Replace NODE-ID in CANDIDATE with an isolated copy of NEXT-SUBTREE.
Repeated replacement of one anchor is last-wins. A later ancestor supersedes
earlier descendants; a later descendant is applied inside an earlier ancestor.
When both OLD-SEMANTIC-KEY and NEW-SEMANTIC-KEY are non-nil, they identify
detached semantic variants of this stable anchor for bounded identity reuse."
(unless (ebox-candidate-p candidate)
(error "Ebox candidate replacement requires an Ebox candidate"))
(when (ebox-candidate--sealed-p candidate)
(error "Ebox candidate is already sealed"))
(unless (eq (null old-semantic-key) (null new-semantic-key))
(error "Ebox semantic replacement requires both old and new keys"))
(let* ((state (ebox-candidate--base-state candidate))
(node-table (plist-get state :node-table))
retained)
(unless (gethash node-id node-table)
(error "Ebox candidate anchor does not exist: %S" node-id))
(unless (and (listp next-subtree) (not (stringp next-subtree)))
(error "Ebox candidate replacement must be an Ebox node"))
(let* ((replacement
(ebox-tree-clear-runtime-identities
(ebox-tree-copy-node-structure next-subtree)))
(source-index
(ebox-tree-source-index
replacement nil nil ebox-incremental--source-base-index)))
;; The new operation supersedes an earlier operation at the same anchor
;; and every earlier descendant operation. An earlier ancestor stays
;; before a new descendant so the descendant is applied inside its
;; candidate-owned subtree at commit time instead of being discarded.
(dolist (entry (ebox-candidate--replacements candidate))
(let ((existing-id
(ebox-incremental--candidate-replacement-anchor-id entry)))
(unless (or (equal existing-id node-id)
(ebox-incremental--candidate-ancestor-p
candidate node-id existing-id))
(push entry retained))))
(let* ((root-id (plist-get (ebox-candidate--base-root candidate)
:node-id))
(root-replaced-p
(cl-find root-id (ebox-candidate--replacements candidate)
:key
#'ebox-incremental--candidate-replacement-anchor-id
:test #'equal)))
(unless (and root-replaced-p (not (equal node-id root-id)))
(setf (ebox-candidate--replacements candidate)
(append
(nreverse retained)
(list
(ebox-incremental--make-candidate-replacement
:anchor-id node-id
:subtree replacement
:source-index source-index
:old-semantic-key old-semantic-key
:new-semantic-key new-semantic-key)))))))
candidate))
(defun ebox-incremental-candidate-replace-root (candidate next-root)
"Replace CANDIDATE's implicit base root with declarative NEXT-ROOT.
The root address is candidate-local and cannot collide with host references.
Root replacement is last-wins and absorbs every descendant replacement."
(unless (ebox-candidate-p candidate)
(error "Ebox root replacement requires an Ebox candidate"))
(ebox-incremental-candidate-replace
candidate
(plist-get (ebox-candidate--base-root candidate) :node-id)
next-root))
(defun ebox-incremental-candidate-replace-host-ref
(candidate host-ref next-subtree
&optional old-semantic-key new-semantic-key)
"Replace HOST-REF in CANDIDATE with declarative NEXT-SUBTREE.
HOST-REF is resolved against the exact published base captured when CANDIDATE
was begun. References introduced only by earlier candidate replacements are
not anchors for the same transaction. OLD-SEMANTIC-KEY and NEW-SEMANTIC-KEY
have the same optional detached-identity meaning as in
`ebox-incremental-candidate-replace'."
(unless (ebox-candidate-p candidate)
(error "Ebox host-ref replacement requires an Ebox candidate"))
(when (ebox-candidate--sealed-p candidate)
(error "Ebox candidate is already sealed"))
(let* ((missing (make-symbol "ebox-missing-host-ref"))
(node-id
(gethash host-ref
(ebox-source--index-host-ref-table-view
(plist-get (ebox-candidate--base-state candidate)
:source-index))
missing)))
(when (eq node-id missing)
(error "Ebox candidate host ref does not exist: %S" host-ref))
(ebox-incremental-candidate-replace
candidate node-id next-subtree
old-semantic-key new-semantic-key)))
(defun ebox-incremental--find-host-ref-node (node host-ref)
"Return the node below NODE that owns HOST-REF, or nil."
(catch 'found
(cl-labels
((visit
(current)
(when (and (listp current) (not (stringp current)))
(when (equal (ebox-tree-node-source-identity current) host-ref)
(throw 'found current))
(dolist (child (ebox-tree--children-raw current))
(visit child)))))
(visit node))
nil))
(defun ebox-incremental-candidate-patch-host-paint
(candidate host-ref previous-subtree next-subtree)
"Patch HOST-REF paint from declarative OLD/NEW subtrees.
PREVIOUS-SUBTREE and NEXT-SUBTREE may be complete Host outputs; only their
local nodes owning HOST-REF are compared. Comparing declarative peers avoids
mistaking inherited/cascaded runtime values for explicit deltas. The update
is accepted only when every changed field is paint-only. Children and runtime
identities remain owned by the candidate base and are path-copied only along
the patched Host's ancestor chain."
(unless (ebox-candidate-p candidate)
(error "Ebox Host paint patch requires an Ebox candidate"))
(when (ebox-candidate--sealed-p candidate)
(error "Ebox candidate is already sealed"))
(let* ((state (ebox-candidate--base-state candidate))
(missing (make-symbol "ebox-missing-host-ref"))
(node-id
(gethash host-ref
(ebox-source--index-host-ref-table-view
(plist-get state :source-index))
missing)))
(when (eq node-id missing)
(error "Ebox candidate host ref does not exist: %S" host-ref))
(catch 'unsupported
(let* ((base-node (gethash node-id (plist-get state :node-table)))
(previous-node
(ebox-incremental--find-host-ref-node
previous-subtree host-ref))
(next-node
(ebox-incremental--find-host-ref-node next-subtree host-ref)))
(when
(cl-some
(lambda (replacement)
(let ((anchor
(ebox-incremental--candidate-replacement-anchor-id
replacement)))
(or (equal anchor node-id)
(ebox-incremental--candidate-ancestor-p
candidate anchor node-id))))
(ebox-candidate--replacements candidate))
(throw 'unsupported nil))
(unless (and previous-node next-node)
(error "Ebox paint patch output lacks Host ref: %S" host-ref))
(unless (and (eq (plist-get base-node :ebox-type)
(plist-get previous-node :ebox-type))
(eq (plist-get previous-node :ebox-type)
(plist-get next-node :ebox-type)))
(throw 'unsupported nil))
(let* ((previous-source-index
(ebox-tree-source-index
previous-subtree nil nil
ebox-incremental--previous-source-base-index))
(next-source-index
(ebox-tree-source-index
next-subtree nil nil ebox-incremental--source-base-index))
(changed-keys
(ebox-incremental--candidate-local-changed-keys
previous-node previous-source-index
next-node next-source-index)))
(unless (cl-every
(lambda (key)
(memq key ebox--paint-style-signature-keys))
changed-keys)
(throw 'unsupported nil))
(let ((patch
(list :ebox-type (plist-get next-node :ebox-type)
:ebox-source-handle
(ebox-tree-node-source-handle next-node))))
(dolist (key changed-keys)
(setq patch
(plist-put patch key
(copy-tree (plist-get next-node key)))))
(dolist (key '(:ebox-style-overrides))
(when (plist-member next-node key)
(setq patch
(plist-put patch key
(copy-tree (plist-get next-node key))))))
(setf (ebox-candidate--paint-patches candidate)
(append
(cl-remove
node-id (ebox-candidate--paint-patches candidate)
:key
#'ebox-incremental--candidate-paint-patch-anchor-id
:test #'equal)
(list
(ebox-incremental--make-candidate-paint-patch
:anchor-id node-id :node patch
:source-index next-source-index
:changed-keys changed-keys))))))
candidate))))
(defun ebox-incremental--candidate-assert-current
(candidate buffer old-state)
"Reject CANDIDATE unless BUFFER still publishes its exact captured base."
(unless (eq buffer (ebox-candidate--buffer candidate))
(error "Ebox candidate belongs to another buffer"))
(unless (eq old-state (ebox-candidate--base-state candidate))
(error "Ebox candidate base state changed"))
(let ((current-state (ebox--buffer-render-state buffer)))
(unless (eq current-state (ebox-candidate--base-state candidate))
(error "Ebox candidate is stale: runtime state changed"))
(unless (eq (plist-get current-state :root-node)
(ebox-candidate--base-root candidate))
(error "Ebox candidate is stale: runtime root changed"))
(unless (equal (or (plist-get current-state :runtime-revision) 0)
(ebox-candidate--base-revision candidate))
(error "Ebox candidate is stale: runtime revision changed"))
(with-current-buffer buffer
(unless (= (buffer-modified-tick)
(ebox-candidate--base-buffer-tick candidate))
(error "Ebox candidate is stale: buffer changed")))))
(defun ebox-incremental--candidate-path-index (root)
"Return node and parent tables for path-copying runtime ROOT.
This intentionally omits root-global semantic validation; only the final
logical tree crosses that boundary."
(let ((node-table (make-hash-table :test 'equal))
(parent-table (make-hash-table :test 'equal)))
(cl-labels
((visit (node parent-id)
(when (and (listp node) (not (stringp node)))
(let ((node-id (ebox--ensure-node-id node)))
(puthash node-id node node-table)
(when parent-id
(puthash node-id parent-id parent-table))
(dolist (child (ebox-tree--children-raw node))
(visit child node-id))))))
(visit root nil))
(list :node-table node-table :parent-table parent-table)))
(defun ebox-incremental--candidate-record-path-copy
(base-node-id next-node parent-id anchor-p structure-p)
"Record one candidate path copy when trace collection is active."
(when (hash-table-p ebox-incremental--candidate-path-copy-trace)
(let* ((origin-table
ebox-incremental--candidate-path-copy-origin-table)
(origin-node-id
(if (hash-table-p origin-table)
(or (gethash base-node-id origin-table) base-node-id)
base-node-id))
(existing
(gethash origin-node-id
ebox-incremental--candidate-path-copy-trace)))
(puthash
origin-node-id
(list :node next-node
:parent-id parent-id
:anchor-p (or anchor-p (plist-get existing :anchor-p))
:structure-p
(or structure-p (plist-get existing :structure-p)))
ebox-incremental--candidate-path-copy-trace)
(when (hash-table-p origin-table)
(puthash origin-node-id origin-node-id origin-table)
(puthash (plist-get next-node :node-id) origin-node-id
origin-table)))))
(defun ebox-incremental--candidate-path-copy-one
(root node-id replacement index)
"Return ROOT path-copied with NODE-ID replaced by REPLACEMENT."
(let* ((node-table (plist-get index :node-table))
(parent-table (plist-get index :parent-table))
(old-child (gethash node-id node-table))
(new-child replacement)
(parent-id (gethash node-id parent-table)))
(unless old-child
(error "Ebox candidate anchor is absent after an ancestor replacement: %S"
node-id))
(ebox-incremental--candidate-record-path-copy
node-id replacement parent-id t
(not (equal (plist-get old-child :node-id)
(plist-get replacement :node-id))))
(while parent-id
(let* ((current-parent-id parent-id)
(old-parent (gethash current-parent-id node-table))
(new-parent
(ebox-tree-copy-with-direct-child-replacements
old-parent (list (cons old-child new-child))))
(grandparent-id
(gethash current-parent-id parent-table)))
(ebox-incremental--candidate-record-path-copy
current-parent-id new-parent grandparent-id nil
(not (equal (plist-get old-child :node-id)
(plist-get new-child :node-id))))
(setq old-child old-parent
new-child new-parent
parent-id grandparent-id)))
(if (equal node-id (plist-get root :node-id))
replacement
new-child)))
(defun ebox-incremental-surface-region-candidate-root
(buffer region-id &optional isolate-owner-subtree-p)
"Return a copy-on-write candidate root for REGION-ID in BUFFER.
Only the region owner and its ancestor path are copied; untouched runtime
subtrees remain shared with the published root. Return nil when the retained
index cannot identify a directly replaceable region owner, leaving callers to
use their ordinary isolated-copy path. ISOLATE-OWNER-SUBTREE-P additionally
copies the owner's descendants before mutation while retaining their ids."
(let* ((state (ebox--buffer-render-state buffer))
(root (plist-get state :root-node))
(node-table (plist-get state :node-table))
(parent-table (plist-get state :parent-table))
(region-node-table (plist-get state :region-node-table))
(owner-id (and (hash-table-p region-node-table)
(gethash region-id region-node-table)))
(owner (and (hash-table-p node-table)
(gethash owner-id node-table)))
(box
(pcase (and owner (plist-get owner :ebox-type))
('box owner)
('flex (plist-get owner :box)))))
(when (and root owner box
(hash-table-p parent-table)
(equal (plist-get box :region-id) region-id))
(let* ((candidate-box
(if isolate-owner-subtree-p
(ebox-tree-copy-node-structure box)
(copy-sequence box)))
(replacement
(if (eq owner box)
candidate-box
(ebox-tree-copy-with-direct-child-replacements
owner (list (cons box candidate-box))))))
(ebox-incremental--candidate-path-copy-one
root owner-id replacement
(list :node-table node-table :parent-table parent-table))))))
(defun ebox-incremental--candidate-path-copy-overlap-many
(root index replacements)
"Return ROOT with possibly nested node-id REPLACEMENTS merged bottom-up.
Unlike `ebox-incremental--candidate-path-copy-many', this helper accepts an
ancestor and its descendant in the same replacement set. A node-local
replacement is applied first, then candidate-owned child paths are installed
into that local copy. Consequently one copied ancestor contains both its own
mutation and every nested mutation, while untouched sibling subtrees remain
shared with the published generation."
(let* ((node-table (plist-get index :node-table))
(parent-table (plist-get index :parent-table))
(affected (make-hash-table :test 'equal))
(children (make-hash-table :test 'equal))
affected-ids)
(maphash
(lambda (node-id _replacement)
(unless (gethash node-id node-table)
(error "Ebox candidate path-copy node is absent: %S" node-id))
(let ((current node-id))
(while current
(unless (gethash current affected)
(puthash current t affected)
(push current affected-ids))
(setq current (gethash current parent-table)))))
replacements)
(let ((depths (make-hash-table :test 'equal)))
(cl-labels
((depth
(node-id)
(or (gethash node-id depths)
(let ((current node-id) path (result -1))
(while (and current (not (gethash current depths)))
(push current path)
(setq current (gethash current parent-table)))
(when current
(setq result (gethash current depths)))
(dolist (path-id path)
(puthash path-id (cl-incf result) depths))
(gethash node-id depths)))))
(setq affected-ids
(sort affected-ids
(lambda (left right)
(> (depth left) (depth right)))))))
(let (result)
(dolist (node-id affected-ids)
(let* ((old-node (gethash node-id node-table))
(local (or (gethash node-id replacements) old-node))
(child-replacements (gethash node-id children))
(effective-child-replacements
(if (or (null child-replacements) (eq local old-node))
child-replacements
(let ((local-children (make-hash-table :test 'equal)))
(ebox-tree-for-each-direct-child
local
(lambda (child)
(puthash (plist-get child :node-id) child
local-children)))
(mapcar
(lambda (replacement)
(let* ((old-child (car replacement))
(child-id (plist-get old-child :node-id))
(local-child (gethash child-id local-children)))
(unless local-child
(error
"Ebox candidate ancestor dropped nested anchor %S"
child-id))
(cons local-child (cdr replacement))))
child-replacements))))
(candidate
(if effective-child-replacements
(ebox-tree-copy-with-direct-child-replacements
local effective-child-replacements)
local))
(parent-id (gethash node-id parent-table)))
(ebox-incremental--candidate-record-path-copy
node-id candidate parent-id (gethash node-id replacements) nil)
(if parent-id
(puthash parent-id
(cons (cons old-node candidate)
(gethash parent-id children))
children)
(setq result candidate))))
(or result root))))
(defun ebox-incremental-surface-scroll-offsets-candidate-root
(buffer region-offsets)
"Return one path-copied BUFFER root carrying REGION-OFFSETS.
REGION-OFFSETS is an alist of region ids and canonical numeric offsets. The
regions may be nested; their owner paths are merged into one candidate root
without mutating the published generation. Return nil when any region cannot
be resolved through the retained runtime indexes."
(let* ((state (ebox--buffer-render-state buffer))
(root (plist-get state :root-node))
(node-table (plist-get state :node-table))
(parent-table (plist-get state :parent-table))
(region-node-table (plist-get state :region-node-table))
(replacements (make-hash-table :test 'equal))
valid)
(setq valid (and root (hash-table-p node-table)
(hash-table-p parent-table)
(hash-table-p region-node-table)))
(dolist (entry region-offsets)
(when valid
(let* ((region-id (car entry))
(offset (cdr entry))
(owner-id (gethash region-id region-node-table))
(owner (and owner-id (gethash owner-id node-table)))
(box
(pcase (and owner (plist-get owner :ebox-type))
('box owner)
((or 'flex 'grid) (plist-get owner :box)))))
(if (and owner-id owner box
(numberp offset)
(equal (plist-get box :region-id) region-id)
(not (gethash owner-id replacements)))
(let* ((candidate-box (copy-sequence box))
(_ (ebox-put candidate-box :scroll-offset offset))
(_ (plist-put candidate-box
:ebox-scroll-offset-controlled-p t))
(replacement
(if (eq owner box)
candidate-box
(ebox-tree-copy-with-direct-child-replacements
owner (list (cons box candidate-box))))))
(puthash owner-id replacement replacements))
(setq valid nil)))))
(when (and valid (> (hash-table-count replacements) 0))
(ebox-incremental--candidate-path-copy-overlap-many
root (list :node-table node-table :parent-table parent-table)
replacements))))
(defun ebox-incremental--candidate-path-copy-many
(root index replacements)
"Return ROOT with node-id REPLACEMENTS applied by one bottom-up path copy."
(let* ((node-table (plist-get index :node-table))
(parent-table (plist-get index :parent-table))
(direct (make-hash-table :test 'equal))
(affected-table (make-hash-table :test 'equal))
affected-ids)
(maphash
(lambda (node-id replacement)
(let ((old-node (gethash node-id node-table))
(parent-id (gethash node-id parent-table)))
(unless old-node
(error "Ebox candidate path-copy node is absent: %S" node-id))
(ebox-incremental--candidate-record-path-copy
node-id replacement parent-id t
(not (equal (plist-get old-node :node-id)
(plist-get replacement :node-id))))
(if parent-id
(puthash parent-id
(cons (cons old-node replacement)
(gethash parent-id direct))
direct)
(puthash node-id replacement direct))
(while parent-id
(unless (gethash parent-id affected-table)
(puthash parent-id t affected-table)
(push parent-id affected-ids))
(setq parent-id (gethash parent-id parent-table)))))
replacements)
(let ((depth-table (make-hash-table :test 'equal)))
(cl-labels
((depth
(node-id)
(or (gethash node-id depth-table)
(let ((current node-id)
path
(result -1))
(while (and current
(not (gethash current depth-table)))
(push current path)
(setq current (gethash current parent-table)))
(when current
(setq result (gethash current depth-table)))
(dolist (path-id path)
(puthash path-id (cl-incf result) depth-table))
(gethash node-id depth-table)))))
(setq affected-ids
(sort affected-ids
(lambda (left right)
(> (depth left) (depth right)))))))
(let ((result (gethash (plist-get root :node-id) replacements)))
(dolist (parent-id affected-ids)
(let* ((child-replacements (gethash parent-id direct))
(old-parent (gethash parent-id node-table))
(copy
(ebox-tree-copy-with-direct-child-replacements
old-parent child-replacements))
(grandparent-id (gethash parent-id parent-table)))
(ebox-incremental--candidate-record-path-copy
parent-id copy grandparent-id nil
(cl-some
(lambda (replacement)
(not (equal (plist-get (car replacement) :node-id)
(plist-get (cdr replacement) :node-id))))
child-replacements))
(if grandparent-id
(puthash grandparent-id
(cons (cons old-parent copy)
(gethash grandparent-id direct))
direct)
(setq result copy))))
(or result root))))
(defun ebox-incremental--candidate-replacements-disjoint-p
(replacements parent-table)
"Return non-nil when REPLACEMENTS have no ancestor relationship.
PARENT-TABLE belongs to the exact candidate base. Every public replacement
anchor is resolved against that base, so disjointness can be proven without
building an index for an intermediate path-copied root."
(cl-labels
((ancestor-p
(ancestor-id descendant-id)
(let ((current (gethash descendant-id parent-table))
found)
(while (and current (not found))
(if (equal current ancestor-id)
(setq found t)
(setq current (gethash current parent-table))))
found)))
(cl-loop
for tail on replacements
always
(cl-loop
for right in (cdr tail)
never
(let ((left-id
(ebox-incremental--candidate-replacement-anchor-id
(car tail)))
(right-id
(ebox-incremental--candidate-replacement-anchor-id right)))
(or (ancestor-p left-id right-id)
(ancestor-p right-id left-id)))))))
(defun ebox-incremental--candidate-replacement-for-anchor
(entries anchor-id)
"Return the replacement in ENTRIES for ANCHOR-ID, or nil."
(cl-find
anchor-id entries
:key #'ebox-incremental--candidate-replacement-anchor-id
:test #'equal))
(defun ebox-incremental--validate-candidate-replacements
(base-index entries)
"Validate ENTRIES' cross-boundary identity against BASE-INDEX.
Each replacement subtree was internally validated when recorded; what
remains is the shared context the full-tree walk used to re-check on
every commit: the replacement's sibling key must stay unique under
its anchor's parent, and every host ref inside a replacement must be
either fresh or owned by a node that some replaced anchor is removing.
Cost is proportional to the replacements, never to the page."
(let* ((node-table (plist-get base-index :node-table))
(parent-table (plist-get base-index :parent-table))
(base-source-index (plist-get base-index :source-index))
(host-ref-table
(ebox-source--index-host-ref-table-view base-source-index))
(replacement-host-refs (make-hash-table :test 'equal))
(replacement-source-indexes (make-hash-table :test #'eq))
(anchor-ids
(mapcar
#'ebox-incremental--candidate-replacement-anchor-id
entries)))
(cl-labels
((ancestor-p (ancestor descendant)
(let ((walk descendant) found)
(while (and walk (not found))
(setq found (equal walk ancestor)
walk (and (not found) (gethash walk parent-table))))
found))
(inside-replaced-anchor-p (node-id)
(let ((walk node-id))
(while (and walk (not (member walk anchor-ids)))
(setq walk (gethash walk parent-table)))
walk))
(collect-host-refs (node acc)
(if (or (not (listp node)) (stringp node))
acc
(let ((ref (ebox-tree-node-source-identity node)))
(when ref (push ref acc)))
(dolist (child (ebox-tree--children-raw node) acc)
(setq acc (collect-host-refs child acc))))))
(dolist (entry entries)
(let* ((node-id
(ebox-incremental--candidate-replacement-anchor-id entry))
(replacement
(ebox-incremental--candidate-replacement-subtree entry))
(replacement-source-index
(or
(ebox-incremental--candidate-replacement-source-index entry)
(gethash replacement replacement-source-indexes)
(puthash replacement
(ebox-tree-source-index replacement)
replacement-source-indexes)))
(replacement-key
(ebox-tree-node-author-key
replacement-source-index replacement))
(parent-id (gethash node-id parent-table))
(parent (and parent-id (gethash parent-id node-table))))
;; Sibling keys stay unique under the anchor's parent.
(when (and replacement-key parent)
(dolist (sibling (ebox-tree--children-raw parent))
(let ((sibling-id (plist-get sibling :node-id)))
(when (and sibling-id
(not (equal sibling-id node-id))
(equal
(if-let*
((sibling-replacement
(ebox-incremental--candidate-replacement-for-anchor
entries sibling-id)))
(let* ((subtree
(ebox-incremental--candidate-replacement-subtree
sibling-replacement))
(index
(or
(ebox-incremental--candidate-replacement-source-index
sibling-replacement)
(gethash subtree
replacement-source-indexes)
(puthash
subtree
(ebox-tree-source-index subtree)
replacement-source-indexes))))
(ebox-tree-node-author-key index subtree))
(ebox-tree-node-author-key
base-source-index sibling))
replacement-key))
(error
"Ebox declarative sibling key %S is not unique"
replacement-key)))))
;; Host refs must be fresh or freed by this same commit.
(when host-ref-table
(dolist (ref (collect-host-refs replacement nil))
(when-let* ((existing-anchor
(gethash ref replacement-host-refs)))
(unless (or (ancestor-p existing-anchor node-id)
(ancestor-p node-id existing-anchor))
(error "Ebox declarative host reference %S is not unique"
ref)))
(puthash ref node-id replacement-host-refs)
(when-let* ((owner-id (gethash ref host-ref-table)))
(unless (inside-replaced-anchor-p owner-id)
(error
"Ebox declarative host reference %S is not unique"
ref))))))))))
(defun ebox-incremental--copy-detached-history (state)
"Return an unpublished detached identity history copied from STATE."
(if-let* ((history (plist-get state :detached-identity-history)))
(ebox-incremental--make-detached-history
:table
(copy-hash-table
(ebox-incremental--detached-history-table history))
:order
(copy-sequence
(ebox-incremental--detached-history-order history))
:node-count
(ebox-incremental--detached-history-node-count history))
(ebox-incremental--make-detached-history
:table (make-hash-table :test 'equal)
:order nil
:node-count 0)))
(defun ebox-incremental--detached-history-store
(history key snapshot)
"Store SNAPSHOT under KEY in bounded detached identity HISTORY."
(let* ((table (ebox-incremental--detached-history-table history))
(existing (gethash key table))
(count (ebox-incremental--detached-history-node-count history)))
(when existing
(cl-decf count (plist-get existing :node-count)))
(puthash key snapshot table)
(cl-incf count (plist-get snapshot :node-count))
(setf (ebox-incremental--detached-history-order history)
(append
(cl-delete key
(ebox-incremental--detached-history-order history)
:test #'equal)
(list key)))
(setf (ebox-incremental--detached-history-node-count history) count)
(while (or (> (hash-table-count table)
(max 0 ebox-incremental--detached-history-entry-limit))
(> (ebox-incremental--detached-history-node-count history)
(max 0 ebox-incremental--detached-history-node-limit)))
(let* ((oldest
(pop (ebox-incremental--detached-history-order history)))
(removed (gethash oldest table)))
(remhash oldest table)
(cl-decf (ebox-incremental--detached-history-node-count history)
(plist-get removed :node-count)))))
history)
(defun ebox-incremental--detached-snapshot-safe-p
(buffer state current-snapshot historical-snapshot)
"Return non-nil when HISTORICAL-SNAPSHOT can reenter BUFFER's STATE.
CURRENT-SNAPSHOT identifies the subtree identities being replaced."
(let ((current-node-ids (plist-get current-snapshot :node-id-set))
(node-table (plist-get state :node-table))
conflict)
(maphash
(lambda (node-id _present)
(when (and (gethash node-id node-table)
(not (gethash node-id current-node-ids)))
(setq conflict t)))
(plist-get historical-snapshot :node-id-set))
(and
(not conflict)
(not
(ebox--runtime-region-id-conflict
(plist-get historical-snapshot :region-id-set)
buffer)))))
(defun ebox-incremental--reconcile-detached-replacement
(candidate state entry old-anchor history)
"Reconcile CANDIDATE's ENTRY with OLD-ANCHOR and STATE.
Use candidate-owned HISTORY for detached identity reuse."
(let* ((replacement
(ebox-incremental--candidate-replacement-subtree entry))
(old-source-index (plist-get state :source-index))
(replacement-source-index
(or (ebox-incremental--candidate-replacement-source-index entry)
(ebox-tree-source-index replacement)))
(old-key
(ebox-incremental--candidate-replacement-old-semantic-key entry))
(new-key
(ebox-incremental--candidate-replacement-new-semantic-key entry))
(anchor-id
(ebox-incremental--candidate-replacement-anchor-id entry)))
(if (or (null history)
(null old-key)
(null new-key)
(not (eq (plist-get old-anchor :ebox-type)
(plist-get replacement :ebox-type))))
(ebox-tree-reconcile-runtime
old-anchor old-source-index replacement replacement-source-index)
(let* ((current (ebox-tree-runtime-identity-snapshot old-anchor))
(old-history-key (list anchor-id old-key))
(new-history-key (list anchor-id new-key)))
(ebox-incremental--detached-history-store
history old-history-key current)
(let ((historical
(gethash
new-history-key
(ebox-incremental--detached-history-table history))))
(if (and historical
(eq (plist-get (plist-get historical :root) :ebox-type)
(plist-get replacement :ebox-type))
(ebox-incremental--detached-snapshot-safe-p
(ebox-candidate--buffer candidate)
state current historical))
(progn
(ebox-tree-reconcile-runtime
(plist-get historical :root)
(ebox-tree-source-index (plist-get historical :root))
replacement replacement-source-index)
(ebox-tree-transfer-runtime-identity
old-anchor replacement))
(ebox-tree-reconcile-runtime
old-anchor old-source-index
replacement replacement-source-index))))))
(ebox-incremental--candidate-replacement-subtree entry))
(defun ebox-incremental--candidate-node-logical-root
(candidate &optional detached-history)
"Return CANDIDATE's complete logical root without copying unchanged subtrees.
DETACHED-HISTORY receives identity snapshots for semantic replacements."
(let ((ebox-incremental--candidate-path-copy-origin-table
(make-hash-table :test 'equal)))
(let* ((root (ebox-candidate--base-root candidate))
(state (ebox-candidate--base-state candidate))
(base-index
(list :node-table (plist-get state :node-table)
:parent-table (plist-get state :parent-table)
:source-index (plist-get state :source-index)))
(entries (ebox-candidate--replacements candidate)))
(if (ebox-incremental--candidate-replacements-disjoint-p
entries (plist-get base-index :parent-table))
(let ((replacements (make-hash-table :test 'equal)))
(ebox-incremental--validate-candidate-replacements
base-index entries)
;; Reconcile every independent anchor against the same published
;; base, then copy their shared ancestor paths in one bottom-up pass.
(dolist (entry entries)
(let* ((node-id
(ebox-incremental--candidate-replacement-anchor-id entry))
(old-anchor
(gethash node-id (plist-get base-index :node-table))))
(unless old-anchor
(error "Ebox candidate anchor is absent: %S" node-id))
;; Replacements were validated when they were recorded
;; by `ebox-candidate-replace-host-ref'.
(puthash
node-id
(ebox-incremental--reconcile-detached-replacement
candidate state entry old-anchor detached-history)
replacements)))
(ebox-incremental--candidate-path-copy-many
root base-index replacements))
;; A descendant refines the candidate-owned ancestor in the same
;; bottom-up path copy. Re-indexing the complete candidate after each
;; replacement repeats work and discards the overlap information that
;; the retained parent table already provides.
(when (cl-some
(lambda (entry)
(ebox-incremental--candidate-replacement-old-semantic-key
entry))
entries)
(error "Ebox semantic candidate replacements must be disjoint"))
(let ((replacements (make-hash-table :test 'equal)))
(ebox-incremental--validate-candidate-replacements
base-index entries)
(dolist (entry entries)
(let* ((node-id
(ebox-incremental--candidate-replacement-anchor-id entry))
(old-anchor
(gethash node-id (plist-get base-index :node-table))))
(unless old-anchor
(error
"Ebox candidate anchor is absent: %S"
node-id))
(puthash
node-id
(ebox-incremental--reconcile-detached-replacement
candidate state entry old-anchor detached-history)
replacements)))
(ebox-incremental--candidate-path-copy-overlap-many
root base-index replacements))))))
(defun ebox-incremental--candidate-reconcile-range-items
(sequence ref items old-source-index item-source-indexes)
"Reconcile ITEMS against REF using explicit generation source indexes."
(let* ((segment (ebox-child-range--lookup-ref sequence ref))
(old-payload (and segment (ebox-child-range--segment-payload segment)))
(key-root (ebox-child-range--sequence-key-root sequence))
(hash-function (ebox-child-range--sequence-hash-function sequence)))
(cl-labels
((item-key
(item)
(let ((index
(gethash (ebox-tree-node-source-handle item)
item-source-indexes)))
(unless index
(error "Ebox Range item has no candidate source index"))
(ebox-tree-node-author-key index item))))
(cl-loop for item in items for offset from 0 do
(let* ((key (item-key item))
(location
(and key
(ebox-child-range--hash-lookup
key-root key (funcall hash-function key))))
(old
(if location
(let ((peer (ebox-child-range--segment-at
sequence (car location))))
(aref (ebox-child-range--segment-payload peer)
(cdr location)))
(and old-payload (< offset (length old-payload))
(aref old-payload offset)))))
(let ((old-key
(and old
(ebox-tree-node-author-key
old-source-index old))))
(when (and old key (null old-key)) (setq old nil))
(when (and old (null key) old-key) (setq old nil))
;; Positional fallback is valid only for unkeyed peers. A
;; disappeared keyed item must never donate its runtime
;; identity to a newly introduced key at the same offset;
;; doing so leaves its old region properties in the candidate
;; output while the new region index has already retired them.
(when (and old key old-key (not (equal old-key key)))
(setq old nil)))
(ebox-tree-reconcile-runtime
old old-source-index item
(gethash (ebox-tree-node-source-handle item)
item-source-indexes)))))
items))
(defun ebox-incremental--candidate-canonical-range-reuse
(sequence ref items reuse-map parent-id segment-index)
"Return canonical ITEMS and effective REUSE-MAP for published SEQUENCE.
Only an item that keeps its exact sequence location may reuse the published
object. A moved item retains semantic identity through ordinary reconcile,
but must re-enter the candidate indexes as a new object."
(let* ((segment (ebox-child-range--lookup-ref sequence ref))
(old-payload
(and segment (ebox-child-range--segment-payload segment)))
(result (vconcat items))
effective)
(dolist (pair reuse-map)
(let ((new-index (car-safe pair))
(old-index (cdr-safe pair)))
(unless (and (integerp new-index) (>= new-index 0)
(< new-index (length result))
(integerp old-index) (>= old-index 0)
old-payload (< old-index (length old-payload)))
(error "Ebox Range reuse map is out of bounds: %S" pair))
(let* ((published (aref old-payload old-index))
(target-location
(list :parent-node-id parent-id
:segment-index segment-index :offset new-index)))
(if (equal (plist-get published :ebox-sequence-location)
target-location)
(progn
(aset result new-index published)
(push pair effective))
;; The producer may use the published object itself to describe
;; an unchanged keyed item. Once that item moves, exact-slot
;; reuse is no longer authorized: leave a detached candidate for
;; ordinary runtime reconciliation, never the published object.
(when (eq (aref result new-index) published)
(aset result new-index
(ebox-tree-clear-runtime-identities
(ebox-tree-copy-node-structure published))))))))
(list (append result nil) (nreverse effective))))
(defun ebox-incremental--candidate-current-node (state node-id)
"Return current candidate NODE-ID from path trace or base STATE."
(or (plist-get (gethash node-id ebox-incremental--candidate-path-copy-trace)
:node)
(gethash node-id (plist-get state :node-table))))
(defun ebox-incremental--candidate-range-ref-overlay (old-state path-trace)
"Return Range ref table updated only for node anchors in PATH-TRACE."
(if (not (hash-table-p path-trace))
(plist-get old-state :range-ref-table)
(let ((parent-table (plist-get old-state :parent-table)) anchors)
(maphash
(lambda (base-id trace)
(when (plist-get trace :anchor-p)
(push (cons base-id (plist-get trace :node)) anchors)))
path-trace)
(let ((anchor-set (make-hash-table :test #'equal)))
(dolist (anchor anchors) (puthash (car anchor) t anchor-set))
(setq anchors
(cl-delete-if
(lambda (anchor)
(let ((walk (gethash (car anchor) parent-table)) found)
(while (and walk (not found))
(setq found (gethash walk anchor-set)
walk (and (not found) (gethash walk parent-table))))
found))
anchors)))
(if (null anchors)
(plist-get old-state :range-ref-table)
(let ((table (copy-hash-table (plist-get old-state :range-ref-table))))
(cl-labels
((walk (node function)
(when-let* ((sequence (plist-get node :ebox-child-sequence)))
(dolist (record (ebox-child-range--range-records sequence))
(funcall function node record)))
(dolist (child (ebox-tree--children-raw node))
(walk child function))))
(dolist (anchor anchors)
(when-let* ((old (gethash (car anchor)
(plist-get old-state :node-table))))
(walk old
(lambda (_parent record)
(remhash (car record) table)))))
(dolist (anchor anchors)
(walk
(cdr anchor)
(lambda (parent record)
(let ((ref (car record)))
(when (gethash ref table)
(error "Ebox child Range ref is not unique: %S" ref))
(puthash ref
(list :parent-node-id (plist-get parent :node-id)
:segment-index (nth 1 record))
table))))))
table)))))
(defun ebox-incremental--candidate-path-copy-overlay
(root node-id replacement state structure-p)
"Path-copy NODE-ID in ROOT using STATE tables plus the candidate trace."
(let* ((parent-table (plist-get state :parent-table))
(old-child (ebox-incremental--candidate-current-node state node-id))
(new-child replacement)
(parent-id (gethash node-id parent-table)))
(ebox-incremental--candidate-record-path-copy
node-id replacement parent-id t structure-p)
(while parent-id
(let* ((current-parent-id parent-id)
(old-parent
(ebox-incremental--candidate-current-node state current-parent-id))
(sequence (plist-get old-parent :ebox-child-sequence))
(location (and sequence
(plist-get old-child :ebox-sequence-location)))
(new-parent
(if location
(let ((copy (copy-sequence old-parent)))
(ebox-tree--put-child-sequence
copy
(ebox-child-range--replace-item-at
sequence (plist-get location :segment-index)
(plist-get location :offset) old-child new-child
(let* ((handle
(ebox-tree-node-source-handle new-child))
(incoming
(and handle
(hash-table-p
ebox-incremental--candidate-incoming-source-indexes)
(gethash
handle
ebox-incremental--candidate-incoming-source-indexes)))
(index
(or incoming (plist-get state :source-index))))
(ebox-tree-node-author-key index new-child))))
(when (eq (plist-get old-parent :ebox-kind) 'box)
(ebox-tree--set-author-style-count
copy
(+ (ebox-tree-author-style-count old-parent)
(- (ebox-tree-author-style-count new-child)
(ebox-tree-author-style-count old-child)))))
copy)
(ebox-tree-copy-with-direct-child-replacements
old-parent (list (cons old-child new-child)))))
(grandparent-id (gethash current-parent-id parent-table)))
(ebox-incremental--candidate-record-path-copy
current-parent-id new-parent grandparent-id nil
(not (equal (plist-get old-child :node-id)
(plist-get new-child :node-id))))
(setq old-child old-parent new-child new-parent
parent-id grandparent-id)))
(if (equal node-id (plist-get root :node-id)) replacement new-child)))
(defun ebox-incremental--candidate-apply-range-replacements (candidate root)
"Apply CANDIDATE Range replacements to ROOT without flattening a parent."
(let* ((entries (ebox-candidate--range-replacements candidate))
(state (ebox-candidate--base-state candidate))
(base-ranges (plist-get state :range-ref-table))
(root-id (plist-get (ebox-candidate--base-root candidate) :node-id))
(root-replaced
(cl-find root-id (ebox-candidate--replacements candidate)
:key #'ebox-incremental--candidate-replacement-anchor-id
:test #'equal))
metrics)
(unless root-replaced
(let ((groups (make-hash-table :test #'equal)) order)
(dolist (entry entries)
(let* ((record (gethash
(ebox-incremental--candidate-range-replacement-ref entry)
base-ranges))
(parent-id (plist-get record :parent-node-id))
(absorbed
(cl-some
(lambda (node-entry)
(let ((anchor
(ebox-incremental--candidate-replacement-anchor-id
node-entry)))
(or (equal anchor parent-id)
(ebox-incremental--candidate-ancestor-p
candidate anchor parent-id))))
(ebox-candidate--replacements candidate))))
(unless absorbed
(unless (gethash parent-id groups) (push parent-id order))
(push entry (gethash parent-id groups)))))
(dolist (parent-id (nreverse order))
(let* ((parent (ebox-incremental--candidate-current-node
state parent-id))
(base-parent (gethash parent-id (plist-get state :node-table)))
(base-sequence
(and base-parent
(plist-get base-parent :ebox-child-sequence)))
(sequence (and parent (plist-get parent :ebox-child-sequence)))
replacements)
;; A node/Host ancestor replacement removes or supersedes the base
;; Range address and therefore wins coalescing.
(when sequence
(dolist (entry (nreverse (gethash parent-id groups)))
(let ((ref
(ebox-incremental--candidate-range-replacement-ref entry)))
(when (ebox-child-range--lookup-ref sequence ref)
(let ((items
(ebox-incremental--candidate-range-replacement-items
entry))
(source-indexes
(ebox-incremental--candidate-range-replacement-source-indexes
entry))
(reuse-map
(ebox-incremental--candidate-range-replacement-reuse-map
entry))
(retain-item-identities-p
(ebox-incremental--candidate-range-replacement-retain-item-identities-p
entry)))
(let ((segment-index
(plist-get (gethash ref base-ranges)
:segment-index)))
(pcase-let
((`(,canonical-items ,effective-reuse-map)
(ebox-incremental--candidate-canonical-range-reuse
sequence ref items reuse-map parent-id
segment-index)))
(setq items canonical-items
reuse-map effective-reuse-map)))
(ebox-incremental--candidate-reconcile-range-items
sequence ref items
(plist-get state :source-index) source-indexes)
(cl-loop for item in items for offset from 0
for location =
(list :parent-node-id parent-id
:segment-index
(plist-get (gethash ref base-ranges)
:segment-index)
:offset offset)
unless (equal
(plist-get item
:ebox-sequence-location)
location)
do (plist-put item :ebox-sequence-location
location))
(push (list ref items reuse-map source-indexes
retain-item-identities-p)
replacements)))))
(when replacements
(let* ((ordered-replacements (nreverse replacements))
(range-pairs
(mapcar (lambda (replacement)
(cons (nth 0 replacement)
(nth 1 replacement)))
ordered-replacements))
(item-source-indexes
(make-hash-table :test #'eq))
(_item-source-indexes
(dolist (replacement ordered-replacements)
(maphash
(lambda (handle index)
(puthash handle index item-source-indexes))
(nth 3 replacement))))
(old-payloads
(mapcar
(lambda (replacement)
(append
(ebox-child-range--segment-payload
(ebox-child-range--lookup-ref
base-sequence (nth 0 replacement))) nil))
ordered-replacements))
(result (ebox-child-range--replace-many
sequence range-pairs
(lambda (item)
(let ((index
(gethash
(ebox-tree-node-source-handle item)
item-source-indexes)))
(unless index
(error
"Ebox Range item has no source index"))
(ebox-tree-node-author-key index item)))
(mapcar
(lambda (replacement)
(cons (nth 0 replacement)
(nth 2 replacement)))
ordered-replacements)
(cl-every
(lambda (replacement)
(nth 4 replacement))
ordered-replacements)))
(new-payloads
(mapcar
(lambda (replacement)
(append
(ebox-child-range--segment-payload
(ebox-child-range--lookup-ref
(car result) (nth 0 replacement))) nil))
ordered-replacements))
(old-affected-payloads
(cl-mapcar
(lambda (replacement payload)
(let ((reused (mapcar #'cdr
(nth 2 replacement))))
(cl-loop for item in payload for index from 0
unless (memq index reused)
collect item)))
ordered-replacements old-payloads))
(new-affected-payloads
(cl-mapcar
(lambda (replacement payload)
(let ((reused (mapcar #'car
(nth 2 replacement))))
(cl-loop for item in payload for index from 0
unless (memq index reused)
collect item)))
ordered-replacements new-payloads))
(children-changed
(not
(cl-every
(lambda (pair)
(equal
(mapcar (lambda (node)
(plist-get node :node-id))
(car pair))
(mapcar (lambda (node)
(plist-get node :node-id))
(cdr pair))))
(cl-mapcar #'cons old-payloads new-payloads))))
(new-parent (copy-sequence parent)))
(ebox-tree--put-child-sequence new-parent (car result))
(when (eq (plist-get parent :ebox-kind) 'box)
(ebox-tree--set-author-style-count
new-parent
(+ (ebox-tree-author-style-count parent)
(- (cl-loop for payload in new-payloads
sum (cl-loop for child in payload
sum (ebox-tree-author-style-count
child)))
(cl-loop for payload in old-payloads
sum (cl-loop for child in payload
sum (ebox-tree-author-style-count
child)))))))
(push (ebox-child-range--metrics-plist (cdr result)) metrics)
(setq root
(ebox-incremental--candidate-path-copy-overlay
root parent-id new-parent state children-changed))
(let ((trace (gethash parent-id
ebox-incremental--candidate-path-copy-trace)))
(plist-put trace :anchor-p nil))
(push (list :old-parent base-parent :new-parent new-parent
:parent-id parent-id
:children-changed children-changed
:old-payloads old-affected-payloads
:new-payloads new-affected-payloads)
ebox-incremental--candidate-range-index-deltas))))))))
(setf (ebox-candidate--range-metrics candidate) (nreverse metrics))
root))
(defun ebox-incremental--candidate-paint-top-anchors
(state patch-table patch-ids)
"Return PATCH-IDS without a patched ancestor in STATE."
(let ((parents (plist-get state :parent-table)) result)
(dolist (node-id patch-ids (nreverse result))
(let ((parent-id (gethash node-id parents)) found)
(while (and parent-id (not found))
(setq found (gethash parent-id patch-table)
parent-id (and (not found) (gethash parent-id parents))))
(unless found (push node-id result))))))
(defun ebox-incremental--candidate-apply-paint-patches (candidate root)
"Apply CANDIDATE paint declarations and copy their dependency closure.
The candidate owns source declarations only. When a patch changes any
schema-inherited or custom property, descendants are path-copied as pending
style consumers; the Style Adapter computes their values exactly once later."
(let* ((state (ebox-candidate--base-state candidate))
(parents (plist-get state :parent-table))
(source-index (plist-get state :source-index))
(patch-table (make-hash-table :test #'equal))
(patch-paths (make-hash-table :test #'equal))
patch-ids)
(dolist (entry (ebox-candidate--paint-patches candidate))
(let ((node-id
(ebox-incremental--candidate-paint-patch-anchor-id entry)))
(puthash node-id entry patch-table)
(push node-id patch-ids)))
(dolist (node-id patch-ids)
(let ((current node-id))
(while current
(puthash current t patch-paths)
(setq current (gethash current parents)))))
(cl-labels
((rewrite
(node inherited-input-changed-p)
(if (or (not (listp node)) (stringp node))
node
(let* ((node-id (plist-get node :node-id))
(entry (and node-id (gethash node-id patch-table)))
(patch (and entry
(ebox-incremental--candidate-paint-patch-node
entry)))
(changed-keys
(and entry
(ebox-incremental--candidate-paint-patch-changed-keys
entry)))
(old-declarations
(ebox-style-node-declarations source-index node))
(next (if (or entry inherited-input-changed-p)
(copy-sequence node)
node)))
(when entry
(plist-put next :ebox-source-handle
(plist-get patch :ebox-source-handle))
(dolist (key changed-keys)
(plist-put next key (copy-tree (plist-get patch key))))
(dolist (key '(:ebox-style-overrides))
(when (plist-member patch key)
(plist-put
next key
(ebox-style-merge-declarations
(copy-tree (plist-get node key))
(copy-tree (plist-get patch key)))))))
(let* ((new-declarations
(ebox-style-node-declarations
(if entry
(ebox-incremental--candidate-paint-patch-source-index
entry)
source-index)
next))
(descendant-input-changed-p
(or inherited-input-changed-p
(and entry
(ebox-style--inherited-declarations-changed-p
old-declarations new-declarations)))))
(when (or entry inherited-input-changed-p)
(ebox-tree--set-author-style-count next 1))
(let (child-replacements)
(dolist (child (ebox-tree--children-raw next))
(when (or descendant-input-changed-p
(gethash (plist-get child :node-id) patch-paths))
(let ((replacement
(rewrite child descendant-input-changed-p)))
(unless (eq replacement child)
(push (cons child replacement)
child-replacements)))))
(when child-replacements
(setq next
(ebox-tree-copy-with-direct-child-replacements
next (nreverse child-replacements)))))
next)))))
(dolist
(node-id
(ebox-incremental--candidate-paint-top-anchors
state patch-table patch-ids)
root)
(let ((current
(ebox-incremental--candidate-current-node state node-id)))
(unless current
(error "Ebox paint patch anchor is absent: %S" node-id))
(setq root
(ebox-incremental--candidate-path-copy-overlay
root node-id
(rewrite current nil)
state nil)))))))
(defun ebox-incremental--candidate-logical-root
(candidate &optional detached-history)
"Return CANDIDATE root after node/Host and child Range coalescing."
(let* ((original-nodes (ebox-candidate--replacements candidate))
(original-ranges (ebox-candidate--range-replacements candidate))
surviving-ranges absorbed)
(dolist (range original-ranges)
(let* ((record
(gethash (ebox-incremental--candidate-range-replacement-ref range)
(plist-get (ebox-candidate--base-state candidate)
:range-ref-table)))
(parent-id (plist-get record :parent-node-id))
(host-wins
(cl-some
(lambda (node-entry)
(let ((anchor
(ebox-incremental--candidate-replacement-anchor-id
node-entry)))
(or (equal anchor parent-id)
(ebox-incremental--candidate-ancestor-p
candidate anchor parent-id))))
original-nodes)))
(unless host-wins (push range surviving-ranges))))
(setq surviving-ranges (nreverse surviving-ranges))
(dolist (range surviving-ranges)
(let* ((record
(gethash (ebox-incremental--candidate-range-replacement-ref range)
(plist-get (ebox-candidate--base-state candidate)
:range-ref-table)))
(parent (gethash (plist-get record :parent-node-id)
(plist-get (ebox-candidate--base-state candidate)
:node-table)))
(segment (and parent
(ebox-child-range--segment-at
(plist-get parent :ebox-child-sequence)
(plist-get record :segment-index)))))
(dolist (item (and segment
(append (ebox-child-range--segment-payload segment)
nil)))
(cl-labels ((collect (node)
(push (plist-get node :node-id) absorbed)
(dolist (child (ebox-tree--children-raw node))
(collect child))))
(collect item)))))
(setf (ebox-candidate--replacements candidate)
(cl-remove-if
(lambda (entry)
(member (ebox-incremental--candidate-replacement-anchor-id entry)
absorbed))
original-nodes)
(ebox-candidate--range-replacements candidate) surviving-ranges)
(unwind-protect
(ebox-incremental--candidate-apply-paint-patches
candidate
(ebox-incremental--candidate-apply-range-replacements
candidate
(ebox-incremental--candidate-node-logical-root
candidate detached-history)))
(setf (ebox-candidate--replacements candidate) original-nodes
(ebox-candidate--range-replacements candidate) original-ranges))))
(defun ebox-incremental-consume-candidate (buffer-or-name candidate)
"Consume CANDIDATE for BUFFER-OR-NAME and return its pure surface input.
The returned plist contains the prepared root, Ebox plan report, identity
policy, and runtime state overrides needed by the TP surface publisher.
CANDIDATE is sealed before target and staleness validation, so every attempted
commit consumes it even when later preparation fails."
(unless (ebox-candidate-p candidate)
(error "Ebox logical commit requires an Ebox candidate"))
(when (ebox-candidate--sealed-p candidate)
(error "Ebox candidate is already sealed"))
(setf (ebox-candidate--sealed-p candidate) t)
(let ((buffer (get-buffer buffer-or-name))
(old-state (ebox-candidate--base-state candidate)))
(unless (buffer-live-p buffer)
(error "Ebox candidate commit requires a live buffer: %S"
buffer-or-name))
(ebox-incremental--candidate-assert-current candidate buffer old-state)
(ebox-incremental--surface-commit-input
buffer old-state
(ebox-incremental--prepare-logical-candidate
buffer old-state candidate))))
(defun ebox-incremental-prepare-root-commit (buffer-or-name next-root)
"Prepare declarative NEXT-ROOT for TP publication in BUFFER-OR-NAME.
Return the same pure surface-input shape as
`ebox-incremental-consume-candidate'."
(let* ((buffer (get-buffer buffer-or-name))
(old-state (and buffer (ebox--buffer-render-state buffer))))
(unless (buffer-live-p buffer)
(error "Ebox declarative commit requires a live buffer: %S"
buffer-or-name))
(unless old-state
(error "Ebox buffer has no rendered runtime: %S" buffer))
(ebox-incremental--surface-commit-input
buffer old-state
(ebox-incremental--prepare-declarative-root
buffer old-state next-root))))
(defun ebox-incremental--hash-keys (table)
"Return the keys currently present in hash TABLE."
(let (keys)
(when (hash-table-p table)
(maphash (lambda (key _value) (push key keys)) table))
(nreverse keys)))
(defun ebox-incremental--hash-snapshot (table keys)
"Return restorable entries for KEYS in hash TABLE."
(let ((missing (make-symbol "missing"))
entries)
(dolist (key keys (nreverse entries))
(let ((value (gethash key table missing)))
(push (list key (not (eq value missing))
(unless (eq value missing) value))
entries)))))
(defun ebox-incremental--restore-hash-snapshot (table entries)
"Restore hash TABLE entries from ENTRIES."
(dolist (entry entries)
(if (nth 1 entry)
(puthash (car entry) (nth 2 entry) table)
(remhash (car entry) table))))
(defun ebox-incremental--replace-hash-entries (target keys source)
"Replace TARGET entries under KEYS with every entry from SOURCE."
(dolist (key keys)
(remhash key target))
(when (hash-table-p source)
(maphash (lambda (key value) (puthash key value target)) source)))
(defun ebox-incremental--buffer-scroll-keys (buffer)
"Return global scroll-state keys currently owned by BUFFER."
(let (keys)
(maphash
(lambda (key state)
(when (eq (plist-get state :buffer) buffer)
(push key keys)))
ebox--scroll-global-state)
(nreverse keys)))
(defun ebox-incremental--candidate-layout-snapshots
(old-state candidate-root candidate-index)
"Seed CANDIDATE-ROOT snapshots from OLD-STATE's published geometry."
(let* ((old-snapshots (plist-get old-state :layout-snapshots))
(old-root (plist-get old-state :root-node))
(old-root-id (plist-get old-root :node-id))
(candidate-root-id (plist-get candidate-root :node-id))
(candidate-node-table (plist-get candidate-index :node-table))
(snapshots (make-hash-table :test 'equal)))
(when (hash-table-p old-snapshots)
(maphash
(lambda (node-id _node)
(when-let* ((snapshot (gethash node-id old-snapshots)))
(puthash node-id (copy-sequence snapshot) snapshots)))
candidate-node-table)
;; A root type replacement receives a fresh runtime id, but its patch
;; owner still covers the currently published complete-buffer geometry.
(unless (equal old-root-id candidate-root-id)
(when-let* ((snapshot (gethash old-root-id old-snapshots)))
(setq snapshot (copy-sequence snapshot))
(setq snapshot (plist-put snapshot :node-id candidate-root-id))
(puthash candidate-root-id snapshot snapshots))))
snapshots))
(defun ebox-incremental--candidate-scroll-state-table
(buffer old-state candidate-index candidate-region-box-table)
"Return retained BUFFER scroll state rebound to the candidate runtime."
(let ((candidate-region-set (plist-get candidate-index :region-id-set))
(scroll-state-table (make-hash-table :test 'equal)))
(dolist
(region-id
(delete-dups
(append (copy-sequence (plist-get old-state :scroll-region-ids))
(ebox-incremental--buffer-scroll-keys buffer))))
(when-let* (((gethash region-id candidate-region-set))
(old-scroll-state (gethash region-id ebox--scroll-global-state))
(candidate-box
(gethash region-id candidate-region-box-table)))
(let ((candidate-scroll-state (copy-sequence old-scroll-state)))
(setq candidate-scroll-state
(plist-put candidate-scroll-state :box candidate-box))
(setq candidate-scroll-state
(plist-put candidate-scroll-state :buffer buffer))
(setq candidate-scroll-state
(ebox--scroll-window-rebind-state-producers
candidate-scroll-state candidate-box))
(puthash region-id candidate-scroll-state scroll-state-table))))
scroll-state-table))
(defun ebox-incremental--node-child-ids-equal-p (old new)
"Return non-nil when OLD and NEW have identical direct child runtime ids."
(let ((old-children (ebox--node-children old))
(new-children (ebox--node-children new))
equal-p)
(setq equal-p t)
(while (and equal-p old-children new-children)
(setq equal-p
(equal (plist-get (car old-children) :node-id)
(plist-get (car new-children) :node-id)))
(setq old-children (cdr old-children)
new-children (cdr new-children)))
(and equal-p (null old-children) (null new-children))))
(defun ebox-incremental--node-child-ids (node)
"Return NODE's direct child runtime ids in render order."
(mapcar #'ebox--ensure-node-id (ebox--node-children node)))
(defun ebox-incremental--pure-child-reorder-p (old-ids new-ids)
"Return non-nil when OLD-IDS and NEW-IDS are one pure permutation."
(when (and (> (length old-ids) 1)
(= (length old-ids) (length new-ids))
(not (equal old-ids new-ids)))
(let ((remaining (make-hash-table :test 'equal))
valid)
(setq valid t)
(dolist (node-id old-ids)
(if (gethash node-id remaining)
(setq valid nil)
(puthash node-id t remaining)))
(dolist (node-id new-ids)
(if (gethash node-id remaining)
(remhash node-id remaining)
(setq valid nil)))
(and valid (zerop (hash-table-count remaining))))))
(defun ebox-incremental--child-splice-p (old-ids new-ids)
"Return non-nil when OLD-IDS and NEW-IDS are one insert or remove run."
(when (and (not (equal old-ids new-ids))
(not (ebox-incremental--pure-child-reorder-p old-ids new-ids)))
(pcase-let ((`(,old-middle ,new-middle)
(ebox-incremental--child-list-middle old-ids new-ids)))
(or (and old-middle (not new-middle))
(and new-middle (not old-middle))))))
(defun ebox-incremental--child-list-middle (old-ids new-ids)
"Return minimal changed middle child id ranges for OLD-IDS and NEW-IDS."
(let* ((old (vconcat old-ids))
(new (vconcat new-ids))
(start 0)
(old-end (length old))
(new-end (length new)))
(while (and (< start old-end)
(< start new-end)
(equal (aref old start) (aref new start)))
(cl-incf start))
(while (and (> old-end start)
(> new-end start)
(equal (aref old (1- old-end))
(aref new (1- new-end))))
(cl-decf old-end)
(cl-decf new-end))
(list (append (cl-subseq old start old-end) nil)
(append (cl-subseq new start new-end) nil))))
(defun ebox-incremental--direct-child-by-id (owner child-id)
"Return OWNER's direct child with runtime CHILD-ID."
(cl-find-if
(lambda (child)
(equal (ebox--ensure-node-id child) child-id))
(ebox--node-children owner)))
(defun ebox-incremental--simple-new-child-splice-p
(buffer owner-id dirty)
"Return non-nil when DIRTY is a cheap vertical child splice."
(when-let* ((owner (ebox--buffer-runtime-node buffer owner-id)))
(pcase-let* ((old-ids (plist-get dirty :old-child-ids))
(new-ids (plist-get dirty :new-child-ids))
(`(,_old-middle ,new-middle)
(ebox-incremental--child-list-middle old-ids new-ids)))
(and (eq (plist-get owner :ebox-type) 'stack)
(eq (ebox-tree-display-inner owner) 'column)
(when-let* ((snapshot
(ebox--ensure-layout-snapshot-spans
buffer owner-id)))
(unless (with-current-buffer buffer
(ebox-buffer--partial-line-slots-p
(plist-get snapshot :buffer-spans)))
(cl-every
(lambda (child-id)
(let ((child
(ebox-incremental--direct-child-by-id
owner child-id)))
(and child (eq (plist-get child :ebox-type) 'box))))
new-middle)))))))
(defun ebox-incremental--child-change-props
(old-node new-node children-changed)
"Return direct-child provenance for OLD-NODE and NEW-NODE when changed."
(when children-changed
(let ((old-ids (ebox-incremental--node-child-ids old-node))
(new-ids (ebox-incremental--node-child-ids new-node)))
(list :old-child-ids old-ids
:new-child-ids new-ids
:pure-child-reorder-p
(ebox-incremental--pure-child-reorder-p old-ids new-ids)
:child-splice-p
(ebox-incremental--child-splice-p old-ids new-ids)))))
(defun ebox-incremental--declarative-dirty-kind (changed-keys)
"Return a declarative dirty kind for CHANGED-KEYS."
(cond
((and changed-keys
(cl-every (lambda (key)
(memq key ebox-tree-metadata-source-keys))
changed-keys))
'metadata)
((and changed-keys
(cl-every (lambda (key)
(or (eq key :surface-properties)
(memq key ebox--paint-style-signature-keys)))
changed-keys))
'paint)
((memq :display changed-keys) 'structure)
(t 'geometry)))
(defun ebox-incremental--candidate-region-box-table (state)
"Return STATE's buffer-owned region boxes from the active side table."
(if-let* ((owned-table (plist-get state :region-box-table))
((hash-table-p owned-table)))
(copy-hash-table owned-table)
(let ((table (make-hash-table :test 'equal)))
(when-let* ((region-id-set (plist-get state :region-id-set)))
(maphash
(lambda (region-id _present)
(when-let* ((box (gethash region-id ebox--region-box-table)))
(puthash region-id box table)))
region-id-set))
table)))
(defun ebox-incremental--candidate-base-index (state)
"Return STATE's persistent indexes without copying unchanged core tables."
(list :node-table (plist-get state :node-table)
:parent-table (plist-get state :parent-table)
:region-id-set (plist-get state :region-id-set)
:region-node-table (plist-get state :region-node-table)
:region-box-count-table (plist-get state :region-box-count-table)
:region-box-table
(ebox-incremental--candidate-region-box-table state)
:range-ref-table (plist-get state :range-ref-table)
:source-index (plist-get state :source-index)
:selector-id-table (plist-get state :selector-id-table)
:selector-class-table (plist-get state :selector-class-table)
:selector-type-table (plist-get state :selector-type-table)
:runtime-type-count-table
(plist-get state :runtime-type-count-table)
:selector-index-stale-p
(plist-get state :selector-index-stale-p)
:native-node-postorder (plist-get state :native-node-postorder)))
(defun ebox-incremental--candidate-copy-index-table (state key test)
"Return a materialized copy of STATE's hash table under KEY."
(let ((table (plist-get state key)))
(if (hash-table-p table)
(copy-hash-table table)
(make-hash-table :test test))))
(defun ebox-incremental--candidate-node-region-record (node)
"Return NODE's `(REGION-ID OWNER-ID BOX)' record, or nil."
(pcase (and (listp node) (plist-get node :ebox-type))
('box
(list (ebox--ensure-region-id node)
(ebox--ensure-node-id node)
node))
('flex
(when-let* ((box (plist-get node :box)))
(list (ebox--ensure-region-id box)
(ebox--ensure-node-id node)
box)))))
(defun ebox-incremental--candidate-adjust-region-box-count
(table node delta)
"Adjust TABLE's box-object region count for NODE by DELTA."
(when (eq (and (listp node) (plist-get node :ebox-type)) 'box)
(let* ((region-id (ebox--ensure-region-id node))
(count (+ (or (gethash region-id table) 0) delta)))
(if (> count 0)
(puthash region-id count table)
(remhash region-id table)))))
(defun ebox-incremental--candidate-region-box-count-table (root)
"Return complete box-object region multiplicities below legacy ROOT."
(let ((table (make-hash-table :test 'equal)))
(cl-labels
((visit (node)
(when (and (listp node) (not (stringp node)))
(ebox-incremental--candidate-adjust-region-box-count
table node 1)
(dolist (child (ebox--node-children node))
(visit child)))))
(visit root))
table))
(defun ebox-incremental--candidate-recompute-regions
(root region-ids region-id-set region-node-table region-box-table)
"Recompute REGION-IDS below ROOT with complete traversal ordering.
This is the compatibility path for duplicated legacy region ids. Core
logical-candidate updates stay local for unique ids; duplicates need the
complete preorder first-owner and postorder last-box semantics."
(maphash
(lambda (region-id _present)
(remhash region-id region-id-set)
(remhash region-id region-node-table)
(remhash region-id region-box-table))
region-ids)
(cl-labels
((visit (node)
(when (and (listp node) (not (stringp node)))
(when-let* ((record
(ebox-incremental--candidate-node-region-record node)))
(pcase-let ((`(,region-id ,owner-id ,_box) record))
(when (gethash region-id region-ids)
(puthash region-id t region-id-set)
(unless (gethash region-id region-node-table)
(puthash region-id owner-id region-node-table)))))
(dolist (child (ebox--node-children node))
(visit child))
(when-let* ((record
(ebox-incremental--candidate-node-region-record node)))
(pcase-let ((`(,region-id ,_owner-id ,box) record))
(when (gethash region-id region-ids)
(puthash region-id box region-box-table)))))))
(visit root)))
(defun ebox-incremental--candidate-native-postorder (root)
"Return ROOT's native compiler postorder without building other indexes."
(let (postorder)
(cl-labels
((visit (node native-layout-p)
(when (and (listp node) (not (stringp node)))
(let ((type (plist-get node :ebox-type)))
(ebox-tree-for-each-direct-child
node
(lambda (child)
(visit child
(and native-layout-p
(not (and (eq type 'flex)
(eq child
(plist-get node :box))))))))
(when native-layout-p
(push node postorder))))))
(visit root t))
(vconcat (nreverse postorder))))
(defun ebox-incremental--candidate-map-native-postorder
(old-state candidate-node-table)
"Re-key OLD-STATE's structurally stable native postorder to candidate nodes."
(let ((old-postorder (plist-get old-state :native-node-postorder)))
(if (vectorp old-postorder)
(vconcat
(mapcar
(lambda (old-node)
(or (gethash (plist-get old-node :node-id)
candidate-node-table)
(error "Ebox candidate lost a structurally retained native node")))
(append old-postorder nil)))
nil)))
(defun ebox-incremental--candidate-range-created-node-ids (range-delta-table)
"Return new node ids covered by structural RANGE-DELTA-TABLE entries."
(let ((old-ids (make-hash-table :test 'equal))
(owned (make-hash-table :test 'equal)))
(when (hash-table-p range-delta-table)
(cl-labels ((record
(node table)
(when (and (listp node) (not (stringp node)))
(puthash (plist-get node :node-id) t table)
(ebox-tree-for-each-direct-child
node (lambda (child) (record child table)))))
(record-new
(node)
(when (and (listp node) (not (stringp node)))
(unless (gethash (plist-get node :node-id) old-ids)
(puthash (plist-get node :node-id) t owned))
(ebox-tree-for-each-direct-child node #'record-new))))
;; Compare identities across the complete Range transaction. A node
;; moved between two Range parents is retained, not removed plus
;; inserted merely because the two structural deltas have different
;; owners.
(maphash
(lambda (_parent-id delta)
(when (plist-get delta :children-changed)
(dolist (payload (plist-get delta :old-payloads))
(dolist (node payload) (record node old-ids)))))
range-delta-table)
(maphash
(lambda (_parent-id delta)
(when (plist-get delta :children-changed)
(dolist (payload (plist-get delta :new-payloads))
(dolist (node payload) (record-new node)))))
range-delta-table)))
owned))
(defun ebox-incremental--candidate-dirty-set-from-touched
(old-state candidate-root candidate-source-index touched
&optional range-delta-table)
"Return declarative dirty entries for path-local TOUCHED nodes."
(let ((old-root (plist-get old-state :root-node))
(range-created-node-ids
(ebox-incremental--candidate-range-created-node-ids range-delta-table))
dirty)
(if (not (equal (plist-get old-root :node-id)
(plist-get candidate-root :node-id)))
(list
(ebox--dirty-entry
(plist-get candidate-root :node-id) 'structure
:changed-keys '(:ebox-type)
:old-region-ids (ebox--node-all-region-ids old-root)
:new-region-ids (ebox--node-all-region-ids candidate-root)
:old old-root :new candidate-root))
(dolist (entry touched)
(pcase-let ((`(,old-node ,new-node ,_parent-id ,children-changed)
entry))
(when (and old-node
(not (gethash (plist-get new-node :node-id)
range-created-node-ids)))
(let* ((changed-keys
(ebox-incremental--candidate-local-changed-keys
old-node (plist-get old-state :source-index)
new-node candidate-source-index))
(range-delta
(and range-delta-table
(gethash (plist-get new-node :node-id)
range-delta-table)))
(kind
(cond
(children-changed 'structure)
(changed-keys
(ebox-incremental--declarative-dirty-kind
changed-keys)))))
(when kind
(push
(append
(ebox--dirty-entry
(plist-get new-node :node-id) kind
:changed-keys
(if children-changed
(cons :children changed-keys)
changed-keys)
:old-region-ids
(and children-changed
(if range-delta
(apply #'append
(mapcar #'ebox--node-all-region-ids
(apply #'append
(plist-get range-delta
:old-payloads))))
(ebox--node-all-region-ids old-node)))
:new-region-ids
(and children-changed
(if range-delta
(apply #'append
(mapcar #'ebox--node-all-region-ids
(apply #'append
(plist-get range-delta
:new-payloads))))
(ebox--node-all-region-ids new-node)))
:old-signature
(ebox-tree-node-local-source-signature old-node)
:new-signature
(ebox-tree-node-local-source-signature new-node))
(if range-delta
(list :range-delta range-delta)
(ebox-incremental--child-change-props
old-node new-node children-changed)))
dirty))))))
(nreverse dirty))))
(defconst ebox-incremental--candidate-full-index-min-touched 64
"Minimum touched nodes before a candidate may rebuild its complete index.")
(defun ebox-incremental--candidate-full-index-p (old-state touched)
"Return non-nil when TOUCHED justifies rebuilding OLD-STATE's index."
(let ((touched-count (length touched))
(old-count (hash-table-count (plist-get old-state :node-table))))
(and (>= touched-count
ebox-incremental--candidate-full-index-min-touched)
(>= (* 2 touched-count) old-count))))
(defun ebox-incremental--candidate-full-index-preparation
(old-state candidate-root touched removed)
"Return broad candidate preparation from TOUCHED and REMOVED nodes."
(let* ((source-index
(ebox-tree-source-index
candidate-root t nil
ebox-incremental--candidate-full-source-base-index))
(index (ebox--runtime-index candidate-root t source-index)))
(setq index (plist-put index :selector-index-stale-p nil))
(list :index index
:dirty-set
(ebox-incremental--candidate-dirty-set-from-touched
old-state candidate-root (plist-get index :source-index) touched)
:participation-node-ids
(ebox-incremental--candidate-participation-node-ids touched)
:touched-count (length touched)
:removed-count (length removed))))
(defun ebox-incremental--candidate-participation-node-ids (touched)
"Return final nodes whose participation context changed in TOUCHED."
(let ((seen (make-hash-table :test 'equal)) result)
(cl-labels ((record
(node)
(when-let* ((node-id (and node (plist-get node :node-id))))
(unless (gethash node-id seen)
(puthash node-id t seen)
(push node-id result)))))
(dolist (entry touched)
(pcase-let ((`(,old-node ,new-node ,_parent-id ,_children-changed)
entry))
(record new-node)
;; Added/replaced children already occur in TOUCHED. Revisit every
;; direct child only when the parent's layout role itself changed.
(when (or (null old-node)
(not (eq (ebox-tree--child-layout-kind old-node 'parent)
(ebox-tree--child-layout-kind new-node 'parent))))
(ebox-tree-for-each-direct-child new-node #'record))))
(nreverse result))))
(defun ebox-incremental--candidate-local-index-delta
(old-state candidate-root &optional path-copy-trace range-index-deltas)
"Return local indexes and dirty entries for CANDIDATE-ROOT.
Traversal stops at every node that is `eq' to OLD-STATE's indexed node. The
work is therefore bounded by copied ancestor paths plus added, removed, and
replacement subtrees. Core tables are materialized in this stage so existing
scalar `gethash' consumers remain unchanged; publication journaling can later
replace those O(n) table copies without changing this delta contract."
(let ((old-root (plist-get old-state :root-node)))
(if (eq candidate-root old-root)
(list :index (ebox-incremental--candidate-base-index old-state)
:dirty-set nil
:touched-count 0
:removed-count 0)
(let* ((old-node-table (plist-get old-state :node-table))
(missing (make-symbol "ebox-candidate-missing-node"))
(removed-seen (make-hash-table :test 'equal))
(visited-new (make-hash-table :test 'equal))
(touched-parent-table (make-hash-table :test 'equal))
touched removed)
(cl-labels
((remove-old-subtree (node)
(let ((node-id (ebox--ensure-node-id node)))
(unless (gethash node-id removed-seen)
(puthash node-id t removed-seen)
(push node removed)
(dolist (child (ebox--node-children node))
(remove-old-subtree child)))))
(record-touched
(old-node new-node parent-id children-changed)
(let ((node-id (ebox--ensure-node-id new-node)))
(puthash node-id parent-id touched-parent-table)
(push (list old-node new-node parent-id children-changed)
touched)))
(visit-new (node parent-id)
(let* ((node-id (ebox--ensure-node-id node))
(old-node (gethash node-id old-node-table missing)))
(unless (or (gethash node-id visited-new)
(eq node old-node))
(puthash node-id t visited-new)
(when (eq old-node missing)
(setq old-node nil))
(let ((children-changed
(or (null old-node)
(not
(ebox-incremental--node-child-ids-equal-p
old-node node)))))
(record-touched
old-node node parent-id children-changed)
(when (and old-node children-changed)
(let ((new-child-ids (make-hash-table :test 'equal)))
(dolist (child (ebox--node-children node))
(puthash (ebox--ensure-node-id child) t
new-child-ids))
(dolist (old-child (ebox--node-children old-node))
(unless (gethash (ebox--ensure-node-id old-child)
new-child-ids)
(remove-old-subtree old-child)))))
(dolist (child (ebox--node-children node))
(visit-new child node-id)))))))
(dolist (delta range-index-deltas)
(let* ((old-parent (plist-get delta :old-parent))
(new-parent (plist-get delta :new-parent))
(parent-id (plist-get delta :parent-id)))
(puthash parent-id t visited-new)
(record-touched old-parent new-parent
(gethash parent-id
(plist-get old-state :parent-table))
(plist-get delta :children-changed))
(dolist (payload (plist-get delta :old-payloads))
(dolist (node payload) (remove-old-subtree node)))
(dolist (payload (plist-get delta :new-payloads))
(dolist (node payload) (visit-new node parent-id)))))
(if (and (hash-table-p path-copy-trace)
(> (hash-table-count path-copy-trace) 0))
(progn
;; Replacement anchors own their affected subtrees. Visit
;; those first so descendant anchors are naturally deduped.
(maphash
(lambda (base-node-id trace)
(when (plist-get trace :anchor-p)
(let* ((old-node (gethash base-node-id old-node-table))
(new-node (plist-get trace :node))
(parent-id (plist-get trace :parent-id)))
(when (and old-node
(not (equal (plist-get old-node :node-id)
(plist-get new-node :node-id))))
(remove-old-subtree old-node))
(visit-new new-node parent-id))))
path-copy-trace)
;; Ancestor copies are known exactly from path construction;
;; indexing them must not probe every untouched sibling.
(maphash
(lambda (base-node-id trace)
(unless (plist-get trace :anchor-p)
(let* ((new-node (plist-get trace :node))
(node-id (plist-get new-node :node-id)))
(unless (gethash node-id visited-new)
(puthash node-id t visited-new)
(record-touched
(gethash base-node-id old-node-table)
new-node
(plist-get trace :parent-id)
(plist-get trace :structure-p))))))
path-copy-trace))
(unless (equal (plist-get old-root :node-id)
(plist-get candidate-root :node-id))
(remove-old-subtree old-root))
(visit-new candidate-root nil)))
;; A type-changing anchor first marks its complete old subtree as
;; removed, then runtime reconciliation may retain keyed descendants
;; under the new type. Those descendants also appear in TOUCHED and
;; must not be debited a second time from core indexes or region
;; multiplicities.
(setq removed
(cl-delete-if
(lambda (node)
(gethash (plist-get node :node-id) visited-new))
(nreverse removed)))
(if (and (null range-index-deltas)
(ebox-incremental--candidate-full-index-p old-state touched))
(let ((ebox-incremental--candidate-full-source-base-index
(ebox-tree-source-fact-index-delta
(plist-get old-state :source-index)
touched removed
ebox-incremental--candidate-incoming-source-indexes)))
(ebox-incremental--candidate-full-index-preparation
old-state candidate-root touched removed))
(progn
;; Local region owner repair needs ancestors before descendants.
;; A complete index rebuild already establishes that order itself.
(let ((depth-table (make-hash-table :test 'equal))
(old-parent-table (plist-get old-state :parent-table)))
(cl-labels
((parent-id
(node-id)
(if (gethash node-id visited-new)
(gethash node-id touched-parent-table)
(gethash node-id old-parent-table)))
(depth
(node-id)
(or (gethash node-id depth-table)
(let ((current node-id)
path
(result -1))
(while (and current
(not (gethash current depth-table)))
(push current path)
(setq current (parent-id current)))
(when current
(setq result (gethash current depth-table)))
(dolist (path-id path)
(puthash path-id (cl-incf result) depth-table))
(gethash node-id depth-table)))))
(setq touched
(sort touched
(lambda (left right)
(< (depth (plist-get (nth 1 left) :node-id))
(depth
(plist-get (nth 1 right) :node-id))))))))
(let* ((node-table
(ebox-incremental--candidate-copy-index-table
old-state :node-table 'equal))
(parent-table
(ebox-incremental--candidate-copy-index-table
old-state :parent-table 'equal))
(region-id-set
(ebox-incremental--candidate-copy-index-table
old-state :region-id-set 'equal))
(region-node-table
(ebox-incremental--candidate-copy-index-table
old-state :region-node-table 'equal))
(old-region-box-count-table
(plist-get old-state :region-box-count-table))
(region-box-count-table
(if (hash-table-p old-region-box-count-table)
(copy-hash-table old-region-box-count-table)
(ebox-incremental--candidate-region-box-count-table
candidate-root)))
(region-box-table
(ebox-incremental--candidate-region-box-table old-state))
(host-ref-table
(copy-hash-table
(ebox-source--index-host-ref-table-view
(plist-get old-state :source-index))))
(runtime-type-count-table
(ebox-incremental--candidate-copy-index-table
old-state :runtime-type-count-table 'eq))
(affected-region-ids (make-hash-table :test 'equal))
(ambiguous-region-ids (make-hash-table :test 'equal)))
(dolist (node removed)
(let ((node-id (plist-get node :node-id)))
(ebox-incremental--adjust-runtime-type-count
runtime-type-count-table node -1)
(remhash node-id node-table)
(remhash node-id parent-table)
(when-let* ((host-ref
(ebox-tree-node-source-identity node)))
(when (equal (gethash host-ref host-ref-table) node-id)
(remhash host-ref host-ref-table)))
(when-let* ((record
(ebox-incremental--candidate-node-region-record node)))
(puthash (car record) t affected-region-ids))
(when (hash-table-p old-region-box-count-table)
(ebox-incremental--candidate-adjust-region-box-count
region-box-count-table node -1))))
;; Remove every old Host binding before installing any new one.
;; Final candidate trees may legally exchange two unique refs; a
;; per-node remove/install loop would reject that valid final state
;; merely because the other old binding had not been visited yet.
(dolist (entry touched)
(when-let* ((old-node (car entry))
(old-host-ref
(ebox-tree-node-source-identity old-node)))
(let* ((new-node (nth 1 entry))
(new-host-ref
(ebox-tree-node-source-identity new-node))
(node-id (plist-get old-node :node-id)))
(unless (equal old-host-ref new-host-ref)
(when (equal (gethash old-host-ref host-ref-table) node-id)
(remhash old-host-ref host-ref-table))))))
(dolist (entry touched)
(pcase-let ((`(,old-node ,new-node ,parent-id ,_children-changed)
entry))
(let* ((node-id (plist-get new-node :node-id))
(old-type (and old-node
(plist-get old-node :ebox-type)))
(new-type (plist-get new-node :ebox-type))
(old-record
(and old-node
(ebox-incremental--candidate-node-region-record
old-node)))
(new-record
(ebox-incremental--candidate-node-region-record new-node))
(stable-unique-region-p
(and old-record new-record
(equal (car old-record) (car new-record))
(= (or (and old-region-box-count-table
(gethash (car old-record)
old-region-box-count-table))
0)
1))))
(when (and old-node (not (eq old-type new-type)))
(ebox-incremental--adjust-runtime-type-count
runtime-type-count-table old-node -1))
(when (and old-record (not stable-unique-region-p))
(puthash (car old-record) t affected-region-ids)
(when (hash-table-p old-region-box-count-table)
(ebox-incremental--candidate-adjust-region-box-count
region-box-count-table old-node -1)))
(puthash node-id new-node node-table)
(when (or (null old-node) (not (eq old-type new-type)))
(ebox-incremental--adjust-runtime-type-count
runtime-type-count-table new-node 1))
(if parent-id
(puthash node-id parent-id parent-table)
(remhash node-id parent-table))
;; Replacement subtrees were validated before recording and
;; checked against untouched Host refs before path copying.
(when-let* ((new-host-ref
(ebox-tree-node-source-identity new-node)))
(unless (and old-node
(equal new-host-ref
(ebox-tree-node-source-identity
old-node)))
(puthash new-host-ref node-id host-ref-table)))
(when (and new-record (not stable-unique-region-p))
(puthash (car new-record) t affected-region-ids))
(when (and (hash-table-p old-region-box-count-table)
(not stable-unique-region-p))
(ebox-incremental--candidate-adjust-region-box-count
region-box-count-table new-node 1)))))
(maphash
(lambda (region-id _present)
(when (or (not (hash-table-p old-region-box-count-table))
(> (or (and old-region-box-count-table
(gethash region-id
old-region-box-count-table))
0)
1)
(> (or (gethash region-id region-box-count-table) 0)
1))
(puthash region-id t ambiguous-region-ids)))
affected-region-ids)
(maphash
(lambda (region-id _present)
(remhash region-id region-id-set)
(remhash region-id region-node-table)
(remhash region-id region-box-table))
affected-region-ids)
;; TOUCHED is preorder, matching `ebox--runtime-index' ownership:
;; a flex owner claims its wrapper before the wrapper box is visited.
(dolist (entry touched)
(when-let* ((record
(ebox-incremental--candidate-node-region-record
(nth 1 entry))))
(pcase-let ((`(,region-id ,owner-id ,box) record))
(puthash region-id t region-id-set)
(unless (gethash region-id region-node-table)
(puthash region-id owner-id region-node-table))
(puthash region-id box region-box-table))))
(when (> (hash-table-count ambiguous-region-ids) 0)
(ebox-incremental--candidate-recompute-regions
candidate-root ambiguous-region-ids
region-id-set region-node-table region-box-table))
(let* ((range-delta-table (make-hash-table :test #'equal))
(_range-deltas
(dolist (delta range-index-deltas)
(puthash (plist-get delta :parent-id) delta
range-delta-table)))
(candidate-source-index
(let ((ebox-tree--incoming-source-indexes
ebox-incremental--candidate-incoming-source-indexes))
(ebox-source--with-runtime-hosts
(ebox-tree-source-index-delta
(plist-get old-state :source-index)
touched removed node-table parent-table candidate-root)
host-ref-table)))
(dirty-set
(ebox-incremental--candidate-dirty-set-from-touched
old-state candidate-root
candidate-source-index
touched range-delta-table))
(structure-p
(cl-some
(lambda (entry)
(eq (plist-get entry :dirty-kind) 'structure))
dirty-set))
(native-node-postorder
(if (or range-index-deltas structure-p)
(ebox-incremental--candidate-native-postorder
candidate-root)
(ebox-incremental--candidate-map-native-postorder
old-state node-table)))
(index
(list :node-table node-table
:parent-table parent-table
:region-id-set region-id-set
:region-node-table region-node-table
:region-box-count-table region-box-count-table
:region-box-table region-box-table
:range-ref-table
(ebox-incremental--candidate-range-ref-overlay
old-state path-copy-trace)
:source-index candidate-source-index
:selector-id-table
(ebox-source--index-selector-table
candidate-source-index 'id)
:selector-class-table
(ebox-source--index-selector-table
candidate-source-index 'class)
:selector-type-table
(ebox-source--index-selector-table
candidate-source-index 'type)
:runtime-type-count-table runtime-type-count-table
:selector-index-stale-p t
:native-node-postorder native-node-postorder)))
(list :index index
:dirty-set dirty-set
:participation-node-ids
(ebox-incremental--candidate-participation-node-ids touched)
:touched-node-ids
(mapcar
(lambda (entry)
(plist-get (nth 1 entry) :node-id))
touched)
:removed-node-ids
(mapcar
(lambda (node) (plist-get node :node-id))
removed)
:touched-count (length touched)
:removed-count (length removed))))))))))
(defun ebox-incremental--candidate-shares-published-node-p
(old-state candidate-root)
"Return non-nil when CANDIDATE-ROOT shares a published runtime node.
The search stops at the first shared node, so a copy-on-write candidate is
recognized from its first untouched sibling without walking the whole tree."
(let ((old-root (plist-get old-state :root-node))
(old-node-table (plist-get old-state :node-table)))
(and (hash-table-p old-node-table)
(catch 'shared
(cl-labels
((visit (node)
(when (and (listp node) (not (stringp node)))
(let ((node-id (plist-get node :node-id)))
(when (and node-id
(not (eq node old-root))
(eq node (gethash node-id old-node-table)))
(throw 'shared t)))
(dolist (child (ebox-tree--children-raw node))
(visit child)))))
(visit candidate-root)
nil)))))
(defun ebox-incremental--plist-changed-keys (old new)
"Return ordered keys whose presence or value differs in OLD and NEW plists."
(let (seen changed)
(dolist (source (list old new))
(cl-loop for key in source by #'cddr
unless (memq key seen)
do (push key seen)
do (let ((old-entry (plist-member old key))
(new-entry (plist-member new key)))
(unless (and (eq (not (null old-entry))
(not (null new-entry)))
(equal-including-properties
(cadr old-entry) (cadr new-entry)))
(push key changed)))))
(nreverse changed)))
(defun ebox-incremental--declaration-changed-keys (old new)
"Return engine longhands changed between canonical OLD and NEW declarations."
(delete-dups
(cl-mapcan
#'ebox-style--engine-projection-targets
(ebox-incremental--plist-changed-keys old new))))
(defun ebox-incremental--candidate-source-record (source-index node)
"Return NODE's borrowed immutable record from candidate SOURCE-INDEX.
Private generated layout nodes have no source handle. A canonical node whose
handle is absent from its generation index violates the candidate boundary."
(when-let* ((handle (ebox-tree-node-source-handle node)))
(or (ebox-source--index-record source-index handle)
(error "Ebox candidate source handle is absent from its index"))))
(defun ebox-incremental--candidate-local-changed-keys
(old old-source-index new new-source-index)
"Return real source changes between OLD and NEW generation facts.
Published nodes carry computed ECSS longhands while fresh candidate nodes are
still declaration-stage values. Compare non-style facts directly and compare
canonical declarations only after expanding them to the engine longhands used
by impact classification; the declaration container itself is not geometry.
Each immutable source record is resolved once for this node pair."
(let ((old-record
(ebox-incremental--candidate-source-record old-source-index old))
(new-record
(ebox-incremental--candidate-source-record new-source-index new))
seen changed source-changed)
(dolist (source (list old new))
(let ((plist source))
(while plist
(let ((key (pop plist)))
(pop plist)
(unless (or (memq key seen)
(memq key ebox-tree--excluded-source-keys)
(memq key '(:props :raw-props))
(ebox-style--property key))
(push key seen)
(let ((old-entry (plist-member old key))
(new-entry (plist-member new key)))
(unless (and (eq (not (null old-entry))
(not (null new-entry)))
(equal-including-properties
(cadr old-entry) (cadr new-entry)))
(push key changed))))))))
(setq changed (nreverse changed))
(unless (eq old-record new-record)
(cl-labels
((record-change
(key old-value new-value)
(unless (equal-including-properties old-value new-value)
(push key source-changed))))
(record-change
:key (and old-record (ebox-source-record-key old-record))
(and new-record (ebox-source-record-key new-record)))
(record-change
:id (and old-record (ebox-source-record-id old-record))
(and new-record (ebox-source-record-id new-record)))
(record-change
:class (and old-record (ebox-source-record-classes old-record))
(and new-record (ebox-source-record-classes new-record)))
;; ID and key are independent metadata fields above. SourceRecord
;; attributes contain only the author's explicit selector attributes,
;; so their changes are neither recomputed nor reported twice.
(record-change
:selector-attributes
(and old-record (ebox-source-record-attributes old-record))
(and new-record (ebox-source-record-attributes new-record)))
(record-change
:selector-state
(and old-record (ebox-source-record-states old-record))
(and new-record (ebox-source-record-states new-record)))))
(setq changed (nconc changed (nreverse source-changed)))
(let ((old-declarations
(ebox-style--node-declarations-from-record old old-record))
(new-declarations
(ebox-style--node-declarations-from-record new new-record)))
(unless (ebox-style-declarations-equal-p
old-declarations new-declarations)
(dolist (key (ebox-incremental--declaration-changed-keys
old-declarations new-declarations))
(unless (memq key changed)
(setq changed (append changed (list key)))))))
changed))
(defun ebox-incremental--declarative-dirty-set
(old-state candidate-root candidate-index)
"Return source-level dirty entries from OLD-STATE to CANDIDATE-ROOT.
CANDIDATE-INDEX is the prepared runtime index for CANDIDATE-ROOT."
(let ((old-table (plist-get old-state :node-table))
(new-table (plist-get candidate-index :node-table))
(old-root (plist-get old-state :root-node))
dirty)
(if (not (equal (plist-get old-root :node-id)
(plist-get candidate-root :node-id)))
(push (ebox--dirty-entry
(plist-get candidate-root :node-id) 'structure
:changed-keys '(:ebox-type)
:old-region-ids (ebox--node-all-region-ids old-root)
:new-region-ids (ebox--node-all-region-ids candidate-root)
:old old-root :new candidate-root)
dirty)
(maphash
(lambda (node-id new-node)
(when-let* ((old-node (gethash node-id old-table)))
(let* ((changed-keys
(ebox-incremental--candidate-local-changed-keys
old-node (plist-get old-state :source-index)
new-node (plist-get candidate-index :source-index)))
(children-changed
(not
(ebox-incremental--node-child-ids-equal-p
old-node new-node)))
(kind (cond
(children-changed 'structure)
(changed-keys
(ebox-incremental--declarative-dirty-kind
changed-keys)))))
(when kind
(push
(append
(ebox--dirty-entry
node-id kind
:changed-keys
(if children-changed
(cons :children changed-keys)
changed-keys)
:old-region-ids
(and children-changed
(ebox--node-all-region-ids old-node))
:new-region-ids
(and children-changed
(ebox--node-all-region-ids new-node))
:old-signature
(ebox-tree-node-local-source-signature old-node)
:new-signature
(ebox-tree-node-local-source-signature new-node))
(ebox-incremental--child-change-props
old-node new-node children-changed))
dirty)))))
new-table))
(nreverse dirty)))
(defun ebox-incremental--isolate-candidate-caches (state)
"Copy mutable Ebox caches in unpublished STATE.
Candidate layout may replace entries while proving a publication. Each
candidate therefore owns its cache tables until TP commits the generation."
(dolist (entry '((:render-cache . equal)
(:layout-snapshots . equal)
(:layout-fragments . equal)
(:render-signature-cache . eq)
(:native-fragment-owner-cache . equal)
(:flex-content-min-widths . eq)
(:viewport-height-dependent-subtree-cache . eq)))
(let ((key (car entry))
(value (plist-get state (car entry))))
(when (hash-table-p value)
(plist-put state key (copy-hash-table value)))))
state)
(defun ebox-incremental--candidate-state
(old-state candidate-root candidate-index prepared)
"Return unpublished runtime state for a prepared declarative root."
;; Every mutable cache is replaced below and isolated once after the
;; candidate fields are installed. Copying OLD-STATE's cache tables here as
;; well only copied values that were immediately discarded.
(let* ((source-index
(or (plist-get candidate-index :source-index)
(plist-get prepared :source-index)
(ebox-tree-source-index candidate-root)))
(state (copy-sequence old-state)))
(dolist
(entry
`((:root-node ,candidate-root)
(:logical-candidate-p
,(plist-get prepared :logical-candidate-p))
(:layout-snapshots ,(plist-get prepared :layout-snapshots))
(:layout-snapshots-complete-p nil)
(:layout-snapshot-detail-generation
,(or (plist-get prepared :layout-snapshot-detail-generation)
(plist-get old-state :layout-snapshot-detail-generation)
0))
;; A candidate starts without publication authority. A successful
;; span projection installs fresh allocation certificates after its
;; output footprint has been validated.
(:retained-allocation-certificates nil)
(:runtime-revision
,(1+ (or (plist-get old-state :runtime-revision) 0)))
(:display-signature ,(plist-get prepared :display-signature))
(:render-cache ,(plist-get prepared :render-cache))
(:layout-fragments ,(plist-get prepared :layout-fragments))
(:layout-fragments-reuse-p
,(plist-get prepared :layout-fragments-reuse-p))
(:detached-identity-history
,(or (plist-get prepared :detached-identity-history)
(plist-get old-state :detached-identity-history)))
(:render-signature-cache
,(plist-get prepared :render-signature-cache))
(:flex-content-min-widths ,(make-hash-table :test 'eq))
(:viewport-height-dependent-subtree-cache
,(plist-get prepared
:viewport-height-dependent-subtree-cache))
(:viewport-dependent-node-ids-ready
,(and (eq candidate-root (plist-get old-state :root-node))
(plist-get old-state
:viewport-dependent-node-ids-ready)))
(:viewport-dependent-node-ids
,(and (eq candidate-root (plist-get old-state :root-node))
(plist-get old-state :viewport-dependent-node-ids)))
(:viewport-dependent-node-id-axes
,(and (eq candidate-root (plist-get old-state :root-node))
(plist-get old-state
:viewport-dependent-node-id-axes)))
(:reflow-prewarm-scratch nil)
(:scroll-region-ids
,(ebox-incremental--hash-keys
(plist-get prepared :scroll-state-table)))
(:node-table ,(plist-get candidate-index :node-table))
(:parent-table ,(plist-get candidate-index :parent-table))
(:region-id-set ,(plist-get candidate-index :region-id-set))
(:region-node-table
,(plist-get candidate-index :region-node-table))
(:region-box-count-table
,(plist-get candidate-index :region-box-count-table))
(:region-box-table
,(plist-get candidate-index :region-box-table))
(:range-ref-table
,(plist-get candidate-index :range-ref-table))
(:source-index ,source-index)
(:selector-id-table
,(ebox-source--index-selector-table source-index 'id))
(:selector-class-table
,(ebox-source--index-selector-table source-index 'class))
(:selector-type-table
,(ebox-source--index-selector-table source-index 'type))
(:runtime-type-count-table
,(plist-get candidate-index :runtime-type-count-table))
(:selector-index-stale-p
,(plist-get candidate-index :selector-index-stale-p))
(:native-node-postorder
,(plist-get candidate-index :native-node-postorder))))
(setq state
(plist-put state (car entry) (cadr entry))))
(ebox-incremental--isolate-candidate-caches state)))
(defun ebox-incremental--seed-candidate-node-cache
(old-cache old-node-table candidate-node-table)
"Seed an eq-keyed candidate cache from OLD-CACHE by stable node identity."
(let ((candidate-cache (make-hash-table :test 'eq))
(missing (make-symbol "ebox-candidate-cache-missing")))
(when (and (hash-table-p old-cache)
(hash-table-p old-node-table)
(hash-table-p candidate-node-table))
(maphash
(lambda (node-id candidate-node)
(when-let* ((old-node (gethash node-id old-node-table)))
(let ((cached (gethash old-node old-cache missing)))
(unless (eq cached missing)
(puthash candidate-node cached candidate-cache)))))
candidate-node-table))
candidate-cache))
(defun ebox-incremental--copy-pruned-candidate-node-cache
(old-cache old-node-table old-parent-table dirty-set
touched-node-ids removed-node-ids)
"Copy OLD-CACHE and remove stale candidate-local nodes.
TOUCHED-NODE-IDS and REMOVED-NODE-IDS name old runtime nodes that no longer
belong in the candidate tree. DIRTY-SET removes old ancestor-path facts that
must be recomputed in the next publication."
(let ((candidate-cache
(if (hash-table-p old-cache)
(copy-hash-table old-cache)
(make-hash-table :test 'eq))))
(dolist (entry dirty-set)
(ebox--invalidate-runtime-render-signature-path
(plist-get entry :node-id)
old-node-table old-parent-table
candidate-cache nil))
(dolist (node-id (append touched-node-ids removed-node-ids))
(when-let* ((old-node (gethash node-id old-node-table)))
(remhash old-node candidate-cache)))
candidate-cache))
(defun ebox-incremental--candidate-structural-caches
(old-state candidate-index dirty-set &optional local-preparation)
"Return seeded candidate signature caches invalidated by DIRTY-SET."
(let* ((old-node-table (plist-get old-state :node-table))
(old-parent-table (plist-get old-state :parent-table))
(candidate-node-table (plist-get candidate-index :node-table))
(candidate-parent-table (plist-get candidate-index :parent-table))
(touched-node-ids
(plist-get local-preparation :touched-node-ids))
(removed-node-ids
(plist-get local-preparation :removed-node-ids))
(signature-cache
(if touched-node-ids
(ebox-incremental--copy-pruned-candidate-node-cache
(plist-get old-state :render-signature-cache)
old-node-table old-parent-table dirty-set
touched-node-ids removed-node-ids)
(ebox-incremental--seed-candidate-node-cache
(plist-get old-state :render-signature-cache)
old-node-table candidate-node-table)))
(height-cache
(if touched-node-ids
(ebox-incremental--copy-pruned-candidate-node-cache
(plist-get old-state :viewport-height-dependent-subtree-cache)
old-node-table old-parent-table dirty-set
touched-node-ids removed-node-ids)
(ebox-incremental--seed-candidate-node-cache
(plist-get old-state :viewport-height-dependent-subtree-cache)
old-node-table candidate-node-table))))
(unless touched-node-ids
(dolist (entry dirty-set)
(ebox--invalidate-runtime-render-signature-path
(plist-get entry :node-id)
candidate-node-table candidate-parent-table
signature-cache height-cache)))
(list :render-signature-cache signature-cache
:viewport-height-dependent-subtree-cache height-cache)))
(defun ebox-incremental--computed-local-dirty-set
(old-state candidate-index dirty-set)
"Return DIRTY-SET expressed as locally computed render effects.
The Style Adapter has already consumed selector metadata and author
declarations for nodes marked `:ebox-candidate-computed-style-p'. For those
nodes, retain ordinary source changes, record selector fields as metadata, and
derive render changes from the computed fragment style signatures."
(let ((old-nodes (plist-get old-state :node-table))
(new-nodes (plist-get candidate-index :node-table)))
(mapcar
(lambda (entry)
(let* ((node-id (plist-get entry :node-id))
(old-node (and node-id (gethash node-id old-nodes)))
(new-node (and node-id (gethash node-id new-nodes))))
(if (or (eq (plist-get entry :dirty-kind) 'structure)
(not old-node)
(not new-node)
(not (plist-get new-node :ebox-candidate-computed-style-p)))
entry
(let* (;; Source-stage dirty planning already materialized this
;; exact node-local delta. Computed projection consumes it;
;; it must not resolve the same generation facts again.
(source-keys (plist-get entry :changed-keys))
(metadata-keys
(cl-remove-if-not
(lambda (key)
(memq key ebox-tree-metadata-source-keys))
source-keys))
(ordinary-keys
(cl-remove-if
(lambda (key)
(or (ebox-style--property key)
(memq key ebox-tree-metadata-source-keys)))
source-keys))
(style-keys
(ebox-fragment-plist-changed-keys
(ebox-fragment-node-style-signature old-node)
(ebox-fragment-node-style-signature new-node)))
(changed-keys
(delete-dups (append ordinary-keys style-keys)))
(next (copy-sequence entry)))
(plist-put next :changed-keys changed-keys)
(plist-put next :metadata-changed-keys metadata-keys)
(plist-put next :dirty-kind
(if changed-keys
(ebox-incremental--declarative-dirty-kind
changed-keys)
'metadata))
(plist-put next :old-signature
(ebox-tree-node-local-source-signature old-node))
(plist-put next :new-signature
(ebox-tree-node-local-source-signature new-node))
next))))
dirty-set)))
(defun ebox-incremental--prepare-declarative-runtime
(buffer old-state candidate-root &optional logical-candidate-p
local-preparation scroll-patch-p source-index prepare-all-styles-p)
"Prepare CANDIDATE-ROOT for BUFFER without mutating its published runtime.
When PREPARE-ALL-STYLES-P is non-nil, compute selector-local styles for every
candidate node so a topology-stable native publication has a complete style
certificate."
(let* ((old-root (plist-get old-state :root-node))
(render-cache
(or (plist-get old-state :render-cache)
(make-hash-table :test 'equal)))
(layout-fragments
(or (plist-get old-state :layout-fragments)
(make-hash-table :test 'equal))))
(let* ((candidate-index
(or (plist-get local-preparation :index)
(ebox--runtime-index candidate-root t source-index)))
(candidate-source-index
(or (plist-get candidate-index :source-index)
(plist-get local-preparation :source-index)
(ebox-tree-source-index candidate-root)))
(_source-index
(progn
(plist-put candidate-index :source-index
candidate-source-index)
(plist-put candidate-index :selector-id-table
(ebox-source--index-selector-table
candidate-source-index 'id))
(plist-put candidate-index :selector-class-table
(ebox-source--index-selector-table
candidate-source-index 'class))
(plist-put candidate-index :selector-type-table
(ebox-source--index-selector-table
candidate-source-index 'type))))
(candidate-region-box-table
(plist-get candidate-index :region-box-table))
(style-node-ids
(or (plist-get local-preparation :touched-node-ids)
(and prepare-all-styles-p
(ebox-incremental--hash-keys
(plist-get candidate-index :node-table)))))
(candidate-subject-table
(and style-node-ids
(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)))
(dirty-set source-dirty-set)
(current-display-signature (ebox--current-display-signature)))
(when (or local-preparation prepare-all-styles-p)
;; Participation depends on the final parent relation and, when the
;; local Style Adapter succeeded, on computed values. Validate that
;; final contract once here; Surface consumes the resulting
;; certificate instead of repeating the same indexed walk.
(ebox-tree-validate-indexed-participation
(plist-get candidate-index :node-table)
(plist-get candidate-index :parent-table)
(or (plist-get local-preparation :participation-node-ids)
style-node-ids)
(if styles-prepared-p 'computed 'author)
candidate-source-index))
(when styles-prepared-p
;; The local style adapter has consumed every pending author fact on
;; the touched paths. Clear the exact retained subtree counts before
;; owner planning so a computed local change cannot masquerade as an
;; unresolved cascade change and force root scope.
(ebox-tree-clear-author-style-pending candidate-root))
(unless (equal (plist-get old-state :display-signature)
current-display-signature)
(push
(ebox--dirty-entry
(plist-get candidate-root :node-id) 'geometry
:changed-keys '(:display-signature)
:old-region-ids (ebox--node-all-region-ids old-root)
:new-region-ids (ebox--node-all-region-ids candidate-root))
dirty-set))
;; Dirty-owner planning needs the geometry that is actually published,
;; not geometry recomputed from the candidate source. Freeze only the
;; lightweight text spans here; detail consumers complete footprint and
;; topology fields on demand before the candidate becomes current.
(with-current-buffer buffer
(dolist (entry dirty-set)
(when (memq (plist-get entry :dirty-kind) '(geometry structure))
(when (gethash (plist-get entry :node-id)
(plist-get old-state :node-table))
(ebox--ensure-layout-snapshot-spans
buffer (plist-get entry :node-id)))))
(unless (equal (plist-get old-root :node-id)
(plist-get candidate-root :node-id))
(ebox--ensure-layout-snapshot-spans
buffer (plist-get old-root :node-id))))
(let* ((structural-caches
(ebox-incremental--candidate-structural-caches
old-state candidate-index dirty-set local-preparation))
(render-signature-cache
(plist-get structural-caches :render-signature-cache))
(viewport-height-dependent-subtree-cache
(plist-get structural-caches
:viewport-height-dependent-subtree-cache)))
(let* ((candidate-scroll-state-table
(ebox-incremental--candidate-scroll-state-table
buffer old-state candidate-index candidate-region-box-table))
(layout-snapshots
(if scroll-patch-p
;; Scroll changes every buffer-dependent span coordinate.
;; Keep the candidate snapshot table empty and let the
;; next non-scroll proof lazily recapture only the nodes it
;; needs; carrying old detail plists would make hidden
;; descendants point past the new visible buffer.
(make-hash-table :test 'equal)
(ebox-incremental--candidate-layout-snapshots
old-state candidate-root candidate-index))))
(list :root candidate-root
:logical-candidate-p logical-candidate-p
:touched-node-ids
(plist-get local-preparation :touched-node-ids)
:removed-node-ids
(plist-get local-preparation :removed-node-ids)
:range-metrics (plist-get local-preparation :range-metrics)
:range-replacement-count
(plist-get local-preparation :range-replacement-count)
:runtime-index-complete-p
(plist-get local-preparation :runtime-index-complete-p)
:computed-participation-validated-p
(and (or local-preparation prepare-all-styles-p)
styles-prepared-p)
:styles-prepared-p styles-prepared-p
:index candidate-index
:dirty-set dirty-set
:display-signature current-display-signature
:render-cache render-cache
:layout-fragments layout-fragments
:layout-fragments-reuse-p nil
:detached-identity-history
(or (plist-get local-preparation
:detached-identity-history)
(plist-get old-state :detached-identity-history))
:render-signature-cache render-signature-cache
:viewport-height-dependent-subtree-cache
viewport-height-dependent-subtree-cache
:layout-snapshots layout-snapshots
:layout-snapshot-detail-generation
(and scroll-patch-p
(1+ (or (plist-get old-state
:layout-snapshot-detail-generation)
0)))
:scroll-state-table candidate-scroll-state-table))))))
(defun ebox-incremental--prepare-declarative-root
(buffer old-state next-root)
"Prepare newly built NEXT-ROOT for BUFFER without mutating publication."
(unless (and (listp next-root) (not (stringp next-root)))
(error "Ebox declarative root must be an Ebox node"))
(let* ((candidate-root
(ebox-tree-clear-runtime-identities
(ebox-tree-copy-node-structure next-root)))
(candidate-source-index
(ebox-tree-source-index
candidate-root t nil ebox-incremental--source-base-index))
(old-root (plist-get old-state :root-node)))
;; Failed preparation may leave harmless allocator gaps, but cannot publish
;; duplicate runtime identities.
(ebox-tree-reconcile-runtime
old-root (plist-get old-state :source-index)
candidate-root candidate-source-index)
(ebox-incremental--prepare-declarative-runtime
buffer old-state candidate-root nil nil nil candidate-source-index
(eq (plist-get old-state :projection-kind) 'native-frame))))
(defun ebox-incremental--candidate-source-input-map (candidate)
"Return exact source-handle inputs carried by CANDIDATE operations."
(let ((table (make-hash-table :test #'eq)))
(dolist (replacement (ebox-candidate--replacements candidate))
(let ((index
(ebox-incremental--candidate-replacement-source-index
replacement)))
(dolist (handle (ebox-source-index-handles index))
(puthash handle index table))))
(dolist (replacement (ebox-candidate--range-replacements candidate))
(maphash
(lambda (_handle index)
(dolist (handle (ebox-source-index-handles index))
(puthash handle index table)))
(ebox-incremental--candidate-range-replacement-source-indexes
replacement)))
(dolist (patch (ebox-candidate--paint-patches candidate))
(let ((index
(ebox-incremental--candidate-paint-patch-source-index patch)))
(when index
(dolist (handle (ebox-source-index-handles index))
(puthash handle index table)))))
table))
(defun ebox-incremental--prepare-logical-candidate
(buffer old-state candidate)
"Prepare CANDIDATE's path-copied logical root for BUFFER."
(let* ((path-copy-trace (make-hash-table :test 'equal))
(detached-history
(ebox-incremental--copy-detached-history old-state))
(incoming-source-indexes
(ebox-incremental--candidate-source-input-map candidate))
range-index-deltas
(candidate-root
(let ((ebox-incremental--candidate-path-copy-trace path-copy-trace)
(ebox-incremental--candidate-range-index-deltas nil)
(ebox-incremental--candidate-incoming-source-indexes
incoming-source-indexes))
(prog1
(ebox-incremental--candidate-logical-root
candidate detached-history)
(setq range-index-deltas
ebox-incremental--candidate-range-index-deltas))))
(local-preparation
(let ((ebox-incremental--candidate-incoming-source-indexes
incoming-source-indexes))
(ebox-incremental--candidate-local-index-delta
old-state candidate-root path-copy-trace range-index-deltas))))
;; Replaced subtrees were validated when they were recorded; the
;; untouched published remainder was validated when it was itself
;; published, and path copying introduces no new sibling or ref
;; relations outside the replaced anchors. Re-walking the whole
;; tree here made every commit pay one full-page validation.
(setq local-preparation
(plist-put local-preparation
:detached-identity-history detached-history))
(setq local-preparation
(plist-put local-preparation :range-metrics
(copy-tree (ebox-candidate--range-metrics candidate))))
(setq local-preparation
(plist-put local-preparation :range-replacement-count
(length (ebox-candidate--range-replacements candidate))))
(when (ebox-candidate--range-replacements candidate)
(setq local-preparation
(plist-put local-preparation :runtime-index-complete-p t)))
(ebox-incremental--prepare-declarative-runtime
buffer old-state candidate-root t local-preparation)))
(defun ebox-incremental--declarative-tentative-op (buffer entry)
"Return the smallest proof candidate for declarative dirty ENTRY."
(let* ((entry (ebox--dirty-entry-with-impact-vector buffer entry))
(node-id (plist-get entry :node-id))
(dirty-kind (plist-get entry :dirty-kind)))
(if (plist-get entry :range-delta)
(ebox--patch-op 'span-patch node-id :dirty entry)
(if (and (equal (plist-get entry :changed-keys) '(:children))
(or (plist-get entry :pure-child-reorder-p)
(and (plist-get entry :child-splice-p)
(ebox-incremental--simple-new-child-splice-p
buffer node-id entry))))
(if (plist-get entry :pure-child-reorder-p)
(ebox--patch-op 'child-reorder node-id :dirty entry)
(ebox--patch-op 'child-splice node-id :dirty entry))
(pcase dirty-kind
('paint
(ebox--patch-op 'paint-patch node-id :dirty entry))
('geometry
(ebox--patch-op-from-dirty-entry entry buffer))
(_
(if-let* ((span-owner-id
(ebox--span-patch-owner-id-for-dirty-entry entry buffer)))
(ebox--patch-op 'span-patch span-owner-id :dirty entry)
(when (and (eq dirty-kind 'structure)
(not
(ebox-incremental--owner-can-absorb-line-count-change-p
buffer node-id)))
(setq node-id
(or (ebox-incremental--declarative-next-owner-id
buffer node-id entry)
(ebox--buffer-root-node-id buffer))))
(ebox--patch-op 'owner-rerender node-id :dirty entry))))))))
(defun ebox-incremental--coalesce-sibling-line-span-ops (buffer ops)
"Merge sibling span patches sharing one published line in BUFFER.
Two or more span-patch ops whose owners are direct children of the
same parent, when that parent owns exactly one buffer line and is
itself span patchable, become one span patch at the parent: the
line is rerendered and republished once instead of once per child.
The parent proof still verifies the rendered line against the
published slots, so this only changes op granularity, never safety."
(let ((groups (make-hash-table :test 'equal))
(coalesced nil)
(result nil))
(dolist (op ops)
(let ((parent-id
(and (eq (plist-get op :op) 'span-patch)
(ebox--runtime-parent-id
buffer (plist-get op :owner-id)))))
(if parent-id
(push op (gethash parent-id groups))
(push op result))))
(maphash
(lambda (parent-id group)
(if (and (cdr group)
(cl-every
(lambda (op)
(null (cl-intersection
(plist-get (plist-get op :dirty) :changed-keys)
ebox--typography-style-signature-keys)))
group)
(not (equal parent-id (ebox--buffer-root-node-id buffer)))
(when-let* ((snapshot (ebox--ensure-layout-snapshot-spans
buffer parent-id))
(spans (plist-get snapshot :buffer-spans)))
(= (length spans) 1))
(ebox--cached-patchable-owner-p buffer parent-id 'span))
(let ((merged (ebox--patch-op
'span-patch parent-id
:dirty (plist-get (car group) :dirty))))
(dolist (op (cdr group))
(setq merged (ebox--patch-op-merge-dirty merged op)))
(let ((candidate-id parent-id)
contained-owner-id)
(while (and candidate-id (not contained-owner-id))
(if (ebox--contained-partial-flex-owner-p
buffer candidate-id)
(setq contained-owner-id candidate-id)
(setq candidate-id
(ebox--runtime-parent-id buffer candidate-id))))
(when contained-owner-id
(setq merged
(ebox--patch-op
'owner-rerender contained-owner-id
:dirty (plist-get merged :dirty)))))
(push merged coalesced))
(setq result (nconc group result))))
groups)
;; Coalesced parents re-enter dominance merging: a parent op may
;; now cover other surviving descendants.
(dolist (op coalesced)
(setq result (ebox--merge-patch-op-into-set buffer result op)))
result))
(defun ebox-incremental--owner-can-absorb-line-count-change-p
(buffer node-id)
"Return non-nil when NODE-ID could publish a line-count change.
An owner whose spans cover whole buffer lines can replace them with a
different number of lines; a definite containment context can clamp a
descendant's line-count change inside its fixed height. A partial-slot
owner with neither property shares its lines with siblings, so any
render whose line count moved is unpublishable there by construction."
(or (when-let* ((snapshot
(ebox--ensure-layout-snapshot-spans buffer node-id))
(spans (plist-get snapshot :buffer-spans)))
(with-current-buffer buffer
(ebox--line-spans-cover-whole-lines-p spans)))
(ebox--definite-containment-formatting-context-p buffer node-id)))
(defun ebox-incremental--common-runtime-ancestor-id (buffer node-ids)
"Return the lowest common runtime ancestor of NODE-IDS in BUFFER."
(let ((ancestor-id (car node-ids)))
(dolist (node-id (cdr node-ids))
(while (and ancestor-id
(not (or (equal ancestor-id node-id)
(ebox--cached-runtime-ancestor-id-p
buffer ancestor-id node-id))))
(setq ancestor-id
(ebox--runtime-parent-id buffer ancestor-id))))
ancestor-id))
(defun ebox-incremental--formatting-context-reflow-node-p (node)
"Return non-nil when NODE is a material local block context.
The first variable-line slice accepts a real column/stack node or a material
box whose child layout is a column. Flex/Grid allocation contexts remain
outside this proof and continue through the ordinary conservative owner path."
(or (eq (ebox-tree-display-inner node) 'column)
(and (eq (plist-get node :ebox-type) 'box)
(let ((children (ebox-tree-layout-children node)))
(and (= (length children) 1)
(eq (ebox-tree-display-inner (car children))
'column))))))
(defun ebox-incremental--formatting-context-reflow-fast-eligible-p
(buffer old-state _candidate-state candidate-id root-id)
"Return non-nil when a whole-line context proof is worth entering.
Use the already-published snapshot shape as a cheap negative filter. Partial
Grid/card slots are handled by the fixed-slot span proof; they must not pay
the expensive parent-context mount and role scan first."
(let ((parents (plist-get old-state :parent-table))
(node-id candidate-id)
eligible)
(while (and node-id (not (equal node-id root-id)) (not eligible))
(let* ((node (gethash node-id (plist-get old-state :node-table)))
(formatting-p
(and node
(ebox-incremental--formatting-context-reflow-node-p node)))
(snapshot
(and formatting-p
;; Eligibility only needs already-published text spans.
;; Completing footprint/role details for every ancestor
;; made a failed proof scan the buffer repeatedly before
;; the real owner proof even started.
(ebox--ensure-layout-snapshot-spans buffer node-id)))
(spans (and snapshot (plist-get snapshot :buffer-spans))))
(if (not formatting-p)
;; A Grid/Flex/row allocator between the dirty LCA and a whole
;; line context is exactly the boundary this proof refuses.
(setq node-id nil)
(when (and spans
(with-current-buffer buffer
(ebox--line-spans-cover-whole-lines-p spans)))
(setq eligible t))
(unless eligible
(setq node-id (gethash node-id parents)))))
)
eligible))
(defun ebox-incremental--formatting-context-reflow-proof
(buffer old-state candidate-state prepared)
"Return a strict parent-formatting-context proof for PREPARED.
The proof is intentionally narrow: multiple geometry/content owners, stable
topology and parent allocation, no scroll/cascade/overflow, and a whole-line
material block under normal column flow. The returned owner may change block
line count; its outside siblings are shifted by the one scoped replacement."
(let* ((dirty-set (plist-get prepared :dirty-set))
(node-ids
(delete-dups
(cl-mapcan #'ebox--dirty-provenance-node-ids dirty-set)))
(root-id (ebox--buffer-root-node-id buffer))
(parents (plist-get old-state :parent-table))
(candidate-parents (plist-get candidate-state :parent-table))
(candidate-id
(and (>= (length dirty-set) 2)
(ebox-incremental--common-runtime-ancestor-id
buffer node-ids)))
proof)
(when (and candidate-id
(not (equal candidate-id root-id))
(ebox-incremental--formatting-context-reflow-fast-eligible-p
buffer old-state candidate-state candidate-id root-id)
(null (plist-get old-state :scroll-region-ids))
(not (plist-get old-state :cascade-active-p))
(or (not (plist-get old-state :cascade-required-p))
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state dirty-set))
(cl-every
(lambda (entry)
(and (eq (plist-get entry :dirty-kind) 'geometry)
(not (plist-get entry :children))
(null (plist-get entry :node-ids))
(cl-every
(lambda (key)
(or (eq key :content)
(eq key :surface-properties)
(memq key ebox--paint-style-signature-keys)))
(plist-get entry :changed-keys))))
dirty-set))
(while (and candidate-id (not proof)
(not (equal candidate-id root-id)))
(let* ((old-node (gethash candidate-id
(plist-get old-state :node-table)))
(new-node (gethash candidate-id
(plist-get candidate-state :node-table)))
(parent-id (gethash candidate-id parents))
(snapshot
(and old-node
(ebox--ensure-layout-snapshot-details
buffer candidate-id)))
(spans (and snapshot (plist-get snapshot :buffer-spans)))
(block-span
(when-let* ((object-table
(plist-get old-state
:surface-node-object-table))
(object (gethash candidate-id object-table))
(mounts (tp-object-mounts object)))
(cons (apply #'min (mapcar (lambda (mount)
(plist-get mount :start))
mounts))
(apply #'max (mapcar (lambda (mount)
(plist-get mount :end))
mounts)))))
(parent (and parent-id
(gethash parent-id
(plist-get old-state :node-table))))
(parent-new (and parent-id
(gethash parent-id
(plist-get candidate-state :node-table))))
(ancestors-stable-p t)
(walk parent-id))
(while (and walk ancestors-stable-p (not (equal walk root-id)))
(let ((ancestor (gethash walk (plist-get old-state :node-table))))
(when (or (not ancestor)
(memq (ebox-tree-display-inner ancestor) '(row flex grid))
(ebox-tree-node-visible-overflow-p ancestor))
(setq ancestors-stable-p nil)))
(setq walk (gethash walk parents)))
(when (and old-node new-node parent parent-new
(ebox-incremental--formatting-context-reflow-node-p
old-node)
(equal (gethash candidate-id parents)
(gethash candidate-id candidate-parents))
(ebox-incremental--node-child-ids-equal-p
old-node new-node)
(equal (ebox-incremental--layout-slot-style-signature
old-node)
(ebox-incremental--layout-slot-style-signature
new-node))
(numberp (ebox-get old-node :width))
(= (ebox-get old-node :width)
(ebox-get new-node :width))
(eq (ebox-tree-display-inner parent) 'column)
(eq (ebox-tree-display-inner parent-new) 'column)
ancestors-stable-p
spans
block-span
(with-current-buffer buffer
(ebox--line-spans-cover-whole-lines-p spans))
(cl-every
(lambda (node-id)
(or (equal node-id candidate-id)
(ebox--cached-runtime-ancestor-id-p
buffer candidate-id node-id)))
node-ids))
(setq proof
(list :owner-id candidate-id
:parent-id parent-id
:dirty-set (copy-sequence dirty-set)
:snapshot snapshot
:old-spans spans
:old-block-span block-span
:region-ids (plist-get snapshot :region-ids)
:allocated-width (ebox-get old-node :width)
:layout-snapshot-detail-generation
(plist-get old-state
:layout-snapshot-detail-generation))))
(unless proof
(setq candidate-id (gethash candidate-id parents))))))
proof))
(defun ebox-incremental--two-owner-allocation-proof
(buffer old-state candidate-state geometry-prepared)
"Return two disjoint geometry proofs containing one allocation closure.
This narrow fallback runs only after the ordinary combined span proof misses.
It never partitions larger dirty sets, preserving their existing coalescing."
(let ((dirty-set (plist-get geometry-prepared :dirty-set))
(root-id (ebox--buffer-root-node-id buffer))
proofs owner-proofs valid allocation-p)
(setq valid (= (length dirty-set) 2))
;; Prove definite-width local spans first. Their snapshot proof warms the
;; shared ancestor facts subsequently consumed by an auto-width allocation
;; closure, avoiding the same cold ancestor scan in the wider proof.
(dolist (dirty
(sort
(copy-sequence dirty-set)
(lambda (left right)
(let ((left-width
(plist-get (plist-get left :old-signature) :width))
(right-width
(plist-get (plist-get right :old-signature) :width)))
(and (numberp left-width) (not (numberp right-width)))))))
(when valid
(let* ((local-prepared (plist-put (copy-sequence geometry-prepared)
:dirty-set (list dirty)))
(plan
;; One dirty entry needs no dominance/coalescing pass. Build
;; its tentative operation directly; the exact span or
;; allocation proof below still owns authorization.
(when-let* ((op
(ebox-incremental--declarative-tentative-op
buffer dirty)))
(list op)))
(allocation-first-p
(let ((old-width
(plist-get (plist-get dirty :old-signature) :width))
(new-width
(plist-get (plist-get dirty :new-signature) :width)))
;; A stable definite-width content slot is the cheap local
;; span case. Auto-width content must prove its allocator
;; closure anyway, so do that before constructing the more
;; expensive span-footprint proof that usually rejects it.
(or (eq (plist-get (car plan) :op) 'owner-rerender)
(not (and (numberp old-width)
(numberp new-width)
(= old-width new-width))))))
(early-allocation-proof
(and allocation-first-p
(ebox-incremental--allocation-closure-proof
buffer old-state candidate-state local-prepared)))
(span-proof
(and (not early-allocation-proof) plan
(ebox-incremental--span-patch-projection-proof
buffer old-state local-prepared candidate-state plan)))
(allocation-proof
(or early-allocation-proof
(and (not span-proof)
(ebox-incremental--allocation-closure-proof
buffer old-state candidate-state local-prepared))))
(proof (or span-proof allocation-proof))
(ids (and proof
(or (plist-get proof :owner-ids)
(and (plist-get proof :owner-id)
(list (plist-get proof :owner-id))))))
(local-owner-proofs
(and proof
(or (plist-get proof :owner-proofs)
(and (plist-get proof :owner-id) (list proof))))))
(when (and span-proof
(not (plist-get span-proof :owner-proofs)))
(setq proof
(plist-put (copy-sequence proof)
:retain-external-owner-suffix-p t)
local-owner-proofs (list proof)))
(if (and proof ids
(not (member root-id ids))
local-owner-proofs)
(progn
(push proof proofs)
(setq owner-proofs
(append owner-proofs
(copy-sequence local-owner-proofs)))
(setq allocation-p (or allocation-p allocation-proof)))
(setq valid nil)))))
(when (and valid allocation-p
(= (length proofs) 2)
(ebox-incremental--owner-proofs-disjoint-p
buffer owner-proofs))
(setq proofs (nreverse proofs))
(let ((owner-ids
(delete-dups
(mapcar (lambda (proof) (plist-get proof :owner-id))
owner-proofs))))
(list :owner-id (car owner-ids)
:owner-ids owner-ids
:owner-proofs owner-proofs
:dirty-set (copy-sequence dirty-set)
:variable-content-p
(cl-some (lambda (proof)
(plist-get proof :variable-content-p))
proofs)
:ancestor-slot-signature
(cl-mapcan
(lambda (proof)
(copy-sequence
(plist-get proof :ancestor-slot-signature)))
proofs)
:layout-snapshot-detail-generation
(plist-get old-state :layout-snapshot-detail-generation)
:static-cascade-p
(cl-every (lambda (proof)
(plist-get proof :static-cascade-p))
proofs)
:contains-allocation-closure-p t)))))
(defun ebox-incremental--mixed-owner-proof
(buffer old-state candidate-state prepared owner-plan)
"Return a strict mixed geometry/paint proof, or nil.
The geometry half reuses the existing variable-line formatting-context proof
with only geometry entries. Paint owners must be disjoint, retain their
layout/topology facts, and change only the first-slice color declarations.
This function is deliberately proof-only: it never widens a scope or
suppresses the ordinary fallback when any fact is missing."
(let* ((dirty-set (plist-get prepared :dirty-set))
(geometry-dirty
(mapcar
(lambda (entry)
;; A merged provenance entry may carry sibling paint owners in
;; `:node-ids'. Geometry proof must start from the geometry
;; entry's own node, otherwise a second selected row can lift
;; the LCA to the root and forfeit mixed-owner publication.
(let ((copy (copy-sequence entry)))
(plist-put copy :node-ids nil)
copy))
(cl-remove-if-not
(lambda (entry)
(or (eq (plist-get entry :dirty-kind) 'geometry)
(and (eq (plist-get entry :dirty-kind) 'structure)
(plist-get entry :range-delta))))
dirty-set)))
(paint-dirty
(cl-remove-if-not
(lambda (entry) (eq (plist-get entry :dirty-kind) 'paint))
dirty-set))
(geometry-prepared
(and geometry-dirty
(plist-put (copy-sequence prepared)
:dirty-set geometry-dirty)))
(range-dirty
(cl-remove-if-not (lambda (entry)
(plist-get entry :range-delta))
geometry-dirty))
(ordinary-geometry-dirty
(cl-remove-if (lambda (entry)
(plist-get entry :range-delta))
geometry-dirty))
;; Two independent content changes commonly arise from one semantic
;; action (for example a control label plus status text). Try the
;; narrow per-owner allocation union before walking and completing a
;; broader common formatting context that will be discarded when the
;; two local proofs already close exactly.
(geometry-two-owner-proof
(and (= (length geometry-dirty) 2)
(ebox-incremental--two-owner-allocation-proof
buffer old-state candidate-state geometry-prepared)))
(geometry-context-proof
(and (not geometry-two-owner-proof)
(null range-dirty)
(>= (length geometry-dirty) 2)
(ebox-incremental--formatting-context-reflow-proof
buffer old-state candidate-state geometry-prepared)))
(geometry-plan
(and (not geometry-two-owner-proof)
(not geometry-context-proof)
geometry-dirty
(let ((ebox-incremental--candidate-state-for-slot-proof
candidate-state))
(append
(mapcar
(lambda (entry)
(ebox--patch-op 'span-patch
(plist-get entry :node-id)
:dirty entry))
range-dirty)
(and ordinary-geometry-dirty
(ebox-incremental--layout-owner-plan
buffer old-state candidate-state
ordinary-geometry-dirty))))))
(geometry-span-proof
(and geometry-plan
(ebox-incremental--span-patch-projection-proof
buffer old-state geometry-prepared candidate-state
geometry-plan)))
(geometry-allocation-proof
(and (= (length geometry-dirty) 1)
(not geometry-context-proof)
(not geometry-span-proof)
(ebox-incremental--allocation-closure-proof
buffer old-state candidate-state geometry-prepared)))
(geometry-proof
(or geometry-context-proof geometry-span-proof
geometry-allocation-proof geometry-two-owner-proof))
(geometry-kind (and geometry-proof
(if geometry-context-proof
'formatting-context-reflow
'span-patch)))
(geometry-owner-ids
(cond
(geometry-context-proof
(list (plist-get geometry-context-proof :owner-id)))
(geometry-span-proof
(copy-sequence (plist-get geometry-span-proof :owner-ids)))
(geometry-allocation-proof
(copy-sequence
(plist-get geometry-allocation-proof :owner-ids)))
(geometry-two-owner-proof
(copy-sequence
(plist-get geometry-two-owner-proof :owner-ids)))))
(geometry-owner-id (car geometry-owner-ids))
(external-paint-dirty
(and geometry-owner-ids
(cl-remove-if
(lambda (entry)
(cl-some
(lambda (geometry-id)
(let ((node-id (plist-get entry :node-id)))
(or (equal geometry-id node-id)
(ebox--cached-runtime-ancestor-id-p
buffer geometry-id node-id))))
geometry-owner-ids))
paint-dirty)))
(paint-owner-ids
(delete-dups
(cl-mapcan #'ebox--dirty-provenance-node-ids
external-paint-dirty)))
(old-nodes (plist-get old-state :node-table))
(new-nodes (plist-get candidate-state :node-table))
(root-id (ebox--buffer-root-node-id buffer))
(paint-valid-p
(and external-paint-dirty
(cl-every
(lambda (entry)
(and (plist-get entry :node-id)
(cl-every (lambda (key)
(memq key
'(:color :bgcolor
:border-left-color
:border-right-color
:border-top-color
:border-bottom-color)))
(plist-get entry :changed-keys))
(let* ((node-id (plist-get entry :node-id))
(old-node (gethash node-id old-nodes))
(new-node (gethash node-id new-nodes)))
(and old-node new-node
(ebox-incremental--node-child-ids-equal-p
old-node new-node)
(equal (gethash node-id
(plist-get old-state :parent-table))
(gethash 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))
(equal (ebox-get old-node :width)
(ebox-get new-node :width))
(equal (ebox-get old-node :height)
(ebox-get new-node :height))
(equal (plist-get
(plist-get entry :old-signature)
:content)
(plist-get
(plist-get entry :new-signature)
:content))))))
external-paint-dirty)))
(disjoint-p
(and geometry-owner-ids
(not (cl-some (lambda (geometry-id)
(member geometry-id paint-owner-ids))
geometry-owner-ids))))
(allocation-disjoint-p
(or (not (or geometry-allocation-proof
geometry-two-owner-proof))
(ebox-incremental--allocation-closure-paint-disjoint-p
old-state geometry-owner-ids paint-owner-ids)))
(cascade-safe-p
(or (and (not (plist-get old-state :cascade-active-p))
(not (plist-get old-state :cascade-required-p))
(not (ebox-style-cascade-active-p)))
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state dirty-set))))
(when (and geometry-span-proof
(not (plist-get geometry-span-proof :owner-proofs)))
;; A detached single owner is rendered outside its ancestor wrappers.
;; Preserve the committed external owner suffix when this proof joins a
;; mixed geometry/paint publication, just as the two-owner union does.
(setq geometry-proof
(plist-put (copy-sequence geometry-span-proof)
:retain-external-owner-suffix-p t)))
(when (and geometry-proof paint-valid-p disjoint-p
allocation-disjoint-p cascade-safe-p
(not (plist-get prepared :scroll-state-transaction))
(not (plist-get prepared :scroll-patch-fast-p))
root-id
(not (equal geometry-owner-id root-id))
(> (hash-table-count old-nodes)
(length (delete-dups (append geometry-owner-ids
paint-owner-ids)))))
(list :geometry-proof geometry-proof
:geometry-kind geometry-kind
:geometry-owner-ids geometry-owner-ids
:geometry-owner-id geometry-owner-id
:paint-owner-ids paint-owner-ids
:owner-ids (append geometry-owner-ids paint-owner-ids)
:dirty-set (copy-sequence dirty-set)
:owner-plan (copy-tree owner-plan)
:outside-complement-count
(- (hash-table-count old-nodes)
(length (delete-dups (append geometry-owner-ids
paint-owner-ids))))))))
(defun ebox-incremental--allocation-closure-paint-disjoint-p
(state geometry-owner-ids paint-owner-ids)
"Return non-nil when paint cannot overwrite geometry closure output.
Paint owned by an ancestor of a geometry closure is safe: mixed projection
recomposes that semantic contribution from the fragment baseline after the
closure is rendered. Paint at the geometry owner or below it is rejected,
because it could address a run whose topology the closure just replaced."
(let ((parents (plist-get state :parent-table))
(geometry-set (make-hash-table :test 'equal))
(valid t))
(dolist (node-id geometry-owner-ids) (puthash node-id t geometry-set))
(dolist (node-id paint-owner-ids)
(let ((walk node-id))
(while (and valid walk)
(when (gethash walk geometry-set) (setq valid nil))
(setq walk (and valid (gethash walk parents))))))
valid))
(defun ebox-incremental--allocation-closure-ancestor-stable-p
(old-state candidate-state owner-id)
"Return non-nil when OWNER-ID's retained path through the root is stable."
(let ((old-nodes (plist-get old-state :node-table))
(new-nodes (plist-get candidate-state :node-table))
(old-parents (plist-get old-state :parent-table))
(new-parents (plist-get candidate-state :parent-table))
(node-id owner-id)
(valid t))
(while (and valid node-id)
(let* ((old-node (gethash node-id old-nodes))
(new-node (gethash node-id new-nodes))
(old-parent (gethash node-id old-parents)))
(setq valid
(and old-node new-node
(equal old-parent (gethash node-id new-parents))
(ebox-incremental--node-child-ids-equal-p
old-node new-node)
(equal
(ebox-incremental--layout-slot-style-signature old-node)
(ebox-incremental--layout-slot-style-signature new-node))
(not (eq (ebox-get
(ebox-fragment-style-source-node old-node)
:overflow)
'visible))
(not (eq (ebox-get
(ebox-fragment-style-source-node new-node)
:overflow)
'visible))
(not (ebox-tree-node-visible-overflow-p old-node))
(not (ebox-tree-node-visible-overflow-p new-node))))
(setq node-id old-parent)))
valid))
(defun ebox-incremental--allocation-closure-proof
(buffer old-state candidate-state prepared)
"Return a strict retained Flex allocation closure proof, or nil."
(let* ((dirty-set (plist-get prepared :dirty-set))
(dirty (and (= (length dirty-set) 1) (car dirty-set)))
(dirty-id (and dirty (plist-get dirty :node-id)))
(changed-keys (and dirty (plist-get dirty :changed-keys)))
(old-nodes (plist-get old-state :node-table))
(new-nodes (plist-get candidate-state :node-table))
(parents (plist-get old-state :parent-table))
(candidate-parents (plist-get candidate-state :parent-table))
(objects (plist-get old-state :surface-node-object-table))
(root-id (ebox--buffer-root-node-id buffer))
(candidate-id dirty-id)
(saw-flex-p nil)
(path-valid-p t)
proof)
(when (and dirty dirty-id root-id
(eq (plist-get dirty :dirty-kind) 'geometry)
(memq :content changed-keys)
(not (plist-get dirty :children))
(null (plist-get dirty :node-ids))
(cl-every
(lambda (key)
(or (eq key :content)
(eq key :surface-properties)
(memq key ebox--paint-style-signature-keys)))
changed-keys)
(ebox-incremental--same-root-surface-identity-p
old-state candidate-state)
(ebox-incremental--span-patch-identity-stable-p
buffer old-state candidate-state prepared dirty-id)
(null (plist-get old-state :scroll-region-ids))
(null (plist-get candidate-state :scroll-region-ids))
(or (and (not (plist-get old-state :cascade-active-p))
(not (plist-get old-state :cascade-required-p))
(not (ebox-style-cascade-active-p)))
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state dirty-set)))
(while (and candidate-id path-valid-p (not proof)
(not (equal candidate-id root-id)))
(let* ((old-node (gethash candidate-id old-nodes))
(new-node (gethash candidate-id new-nodes))
(parent-id (gethash candidate-id parents))
(display (and old-node (ebox-tree-display-inner old-node))))
(setq saw-flex-p (or saw-flex-p (eq display 'flex)))
(setq path-valid-p
(and old-node new-node
(equal parent-id
(gethash candidate-id candidate-parents))
(ebox-incremental--node-child-ids-equal-p
old-node new-node)
(equal
(ebox-incremental--layout-slot-style-signature old-node)
(ebox-incremental--layout-slot-style-signature new-node))
(not (eq (ebox-get
(ebox-fragment-style-source-node old-node)
:overflow)
'visible))
(not (eq (ebox-get
(ebox-fragment-style-source-node new-node)
:overflow)
'visible))
(not (ebox-tree-node-visible-overflow-p old-node))
(not (ebox-tree-node-visible-overflow-p new-node))))
(when (and path-valid-p saw-flex-p
(hash-table-p objects) (gethash candidate-id objects))
(let* ((snapshot
(ebox-incremental--cached-layout-snapshot-details
buffer candidate-id))
(spans (and snapshot (plist-get snapshot :buffer-spans)))
(ancestor-signature
(and spans
(ebox-incremental--ancestor-slot-signature
buffer old-state candidate-state candidate-id)))
(allocated-width
(and snapshot
(plist-get
(plist-get snapshot
:external-footprint-signature)
:max-line-pixel-width)))
(content
(plist-get (plist-get dirty :new-signature) :content)))
(when (and spans ancestor-signature allocated-width
(> allocated-width 0)
(stringp content)
(not (string-match-p "\n" content))
(<= (ebox--string-pixel-width content)
allocated-width)
(ebox--spans-contiguous-lines-p spans)
(with-current-buffer buffer
(ebox--line-spans-cover-whole-lines-p spans))
(ebox-incremental--allocation-closure-ancestor-stable-p
old-state candidate-state candidate-id))
(let ((owner-proof
(list :owner-id candidate-id
:dirty-set (copy-sequence dirty-set)
:changed-keys (copy-sequence changed-keys)
:snapshot snapshot
:allocated-width allocated-width
:allocation-closure-p t
:ancestor-slot-signature ancestor-signature
:layout-snapshot-detail-generation
(plist-get old-state
:layout-snapshot-detail-generation)
:static-cascade-p t)))
(setq proof
(list :owner-ids (list candidate-id)
:owner-proofs (list owner-proof)
:dirty-set (copy-sequence dirty-set)
:ancestor-slot-signature ancestor-signature
:layout-snapshot-detail-generation
(plist-get old-state
:layout-snapshot-detail-generation)
:static-cascade-p t))))))
(setq candidate-id parent-id))))
proof))
(defun ebox-incremental--broad-dirty-owner-id (buffer node-ids)
"Return one structurally safe broad owner for NODE-IDS in BUFFER."
(let* ((root-id (ebox--buffer-root-node-id buffer))
(root (ebox--buffer-runtime-node buffer root-id))
(owner-id
(ebox-incremental--common-runtime-ancestor-id
buffer node-ids)))
(when (and root-id root owner-id)
(if (or (equal owner-id root-id)
(not (eq (ebox-tree-display-inner root) 'column)))
root-id
;; A direct child of a column root owns complete vertical lines.
;; Publishing it can therefore absorb descendant line-count changes
;; without probing every intermediate ancestor's buffer spans.
(while (and owner-id
(not (equal
(ebox--runtime-parent-id buffer owner-id)
root-id)))
(setq owner-id (ebox--runtime-parent-id buffer owner-id)))
owner-id))))
(defun ebox-incremental--runtime-descendant-count (node)
"Return the number of declarative descendants below runtime NODE."
(cl-loop for child in (ebox-tree-node-children node)
sum (1+ (ebox-incremental--runtime-descendant-count child))))
(defun ebox-incremental--broad-dirty-op (buffer render-dirty-set)
"Return one safely proofable owner op for a broad RENDER-DIRTY-SET."
(when (and (>= (length render-dirty-set)
ebox--owner-coalesce-min-count)
(cl-some (lambda (entry)
(not (eq (plist-get entry :dirty-kind) 'paint)))
render-dirty-set))
(let* ((node-ids
(delete-dups
(cl-mapcan #'ebox--dirty-provenance-node-ids
render-dirty-set)))
(owner-id
(ebox-incremental--broad-dirty-owner-id
buffer node-ids)))
(when-let* ((owner (and owner-id
(ebox--buffer-runtime-node buffer owner-id)))
(descendant-count
(ebox-incremental--runtime-descendant-count owner))
((> descendant-count 0))
(dirty-descendant-count
(cl-count-if
(lambda (node-id)
(not (equal node-id owner-id)))
node-ids))
((>= (/ (float dirty-descendant-count) descendant-count)
ebox-incremental--broad-dirty-min-node-coverage)))
(ebox--patch-op
'owner-rerender owner-id
:dirty (ebox--merge-dirty-provenance-list render-dirty-set))))))
(defun ebox-incremental--local-reflow-op (buffer render-dirty-set)
"Return one local wrapper op for related geometry RENDER-DIRTY-SET."
(when (and (> (length render-dirty-set) 1)
(or (= (length render-dirty-set) 2)
(not
(cl-every
(lambda (entry)
(equal (plist-get entry :changed-keys) '(:content)))
render-dirty-set)))
(cl-every
(lambda (entry)
(let ((entry (ebox--dirty-entry-with-impact-vector
buffer entry)))
(and (eq (plist-get entry :dirty-kind) 'geometry)
(memq 'parent-layout
(plist-get entry :impact-vector))
(cl-some
(lambda (node-id)
(ebox--geometry-affects-parent-layout-p
buffer node-id))
(ebox--dirty-provenance-node-ids entry)))))
render-dirty-set))
(let* ((root-id (ebox--buffer-root-node-id buffer))
(node-ids
(delete-dups
(cl-mapcan #'ebox--dirty-provenance-node-ids
render-dirty-set)))
(owner-id
(ebox-incremental--common-runtime-ancestor-id
buffer node-ids)))
(while (and owner-id (not (equal owner-id root-id))
(not (ebox--owned-overflow-coverage-wrapper-node-p
buffer owner-id)))
(setq owner-id (ebox--runtime-parent-id buffer owner-id)))
(when (and owner-id (not (equal owner-id root-id))
(not (equal (ebox--runtime-parent-id buffer owner-id)
root-id)))
(ebox--patch-op
'owner-rerender owner-id
:dirty (ebox--merge-dirty-provenance-list render-dirty-set))))))
(defun ebox-incremental--nearest-fixed-basis-flex-item-owner-id
(buffer node-id)
"Return NODE-ID's nearest content-independent fixed-basis Flex item."
(let ((candidate node-id)
(root-id (ebox--buffer-root-node-id buffer))
owner)
(while (and candidate (not owner) (not (equal candidate root-id)))
(if (ebox--flex-item-fixed-basis-content-allocation-stable-p
buffer candidate)
(setq owner candidate)
(setq candidate (ebox--runtime-parent-id buffer candidate))))
owner))
(defun ebox-incremental--fixed-basis-flex-local-op
(buffer render-dirty-set)
"Return one tentative Flex-item op for related geometry RENDER-DIRTY-SET.
Multiple descendant content changes can share a fixed-basis, zero-minimum Flex
item without changing its main-axis allocation. This planner only selects the
shared owner; grouped proof verifies topology and the retained ancestor slot,
then Surface performs the one actual candidate render and complete footprint,
role, overflow, and outside-complement validation."
(when (and (> (length render-dirty-set) 1)
(cl-every
(lambda (entry)
(and (eq (plist-get entry :dirty-kind) 'geometry)
(not (plist-get entry :children))))
render-dirty-set))
(let* ((owner-ids
(delete-dups
(mapcar
(lambda (entry)
(ebox-incremental--nearest-fixed-basis-flex-item-owner-id
buffer (plist-get entry :node-id)))
render-dirty-set)))
(owner-id (and (= (length owner-ids) 1) (car owner-ids))))
(when (and owner-id
(not (equal owner-id (ebox--buffer-root-node-id buffer))))
(ebox--patch-op
'owner-rerender owner-id
:dirty (ebox--merge-dirty-provenance-list render-dirty-set))))))
(defun ebox-incremental--dirty-set-has-child-splice-p
(buffer render-dirty-set)
"Return non-nil when RENDER-DIRTY-SET contains a provable child splice."
(cl-some
(lambda (entry)
(and (equal (plist-get entry :changed-keys) '(:children))
(plist-get entry :child-splice-p)
(ebox-incremental--simple-new-child-splice-p
buffer (plist-get entry :node-id) entry)))
render-dirty-set))
(defvar ebox-incremental--patch-planner-route 'pure
"Internal M2a patch planner route: `legacy', `pure', or `shadow'.")
(defun ebox-incremental--legacy-tentative-patch-set
(buffer render-dirty-set)
"Return legacy merged owner candidates for RENDER-DIRTY-SET in BUFFER."
(let ((ebox--patchable-owner-cache (make-hash-table :test 'equal))
(ebox--runtime-ancestor-cache (make-hash-table :test 'equal))
(ebox--flex-slot-safety-cache
(or ebox--flex-slot-safety-cache
(make-hash-table :test 'equal)))
(index (ebox--make-patch-op-merge-index))
ops)
(ebox--with-layout-snapshot-detail-context buffer
(if-let* ((coalesced-op
(and (not
(ebox-incremental--dirty-set-has-child-splice-p
buffer render-dirty-set))
(or (ebox-incremental--broad-dirty-op
buffer render-dirty-set)
(ebox-incremental--fixed-basis-flex-local-op
buffer render-dirty-set)
(ebox-incremental--local-reflow-op
buffer render-dirty-set)))))
(setq ops (list coalesced-op))
(dolist (entry render-dirty-set)
(ebox--patch-op-merge-index-add
buffer index
(ebox-incremental--declarative-tentative-op buffer entry)))
(setq ops
(ebox-incremental--coalesce-sibling-line-span-ops
buffer (ebox--patch-op-merge-index-result index)))))
(ebox--dedupe-patch-owners buffer ops)))
(defun ebox-incremental--pure-tentative-patch-set
(buffer render-dirty-set)
"Return pure-port merged owner candidates for RENDER-DIRTY-SET in BUFFER."
(let ((ebox--patchable-owner-cache (make-hash-table :test 'equal))
(ebox--runtime-ancestor-cache (make-hash-table :test 'equal))
(ebox--flex-slot-safety-cache
(or ebox--flex-slot-safety-cache
(make-hash-table :test 'equal)))
(parent-table
(plist-get (ebox--buffer-render-state buffer) :parent-table))
ops)
(unless (hash-table-p parent-table)
(signal 'ebox-patch-plan-error
(list :missing-parent-table buffer)))
(ebox--with-layout-snapshot-detail-context buffer
(if-let* ((coalesced-op
(and (not
(ebox-incremental--dirty-set-has-child-splice-p
buffer render-dirty-set))
(or (ebox-incremental--broad-dirty-op
buffer render-dirty-set)
(ebox-incremental--fixed-basis-flex-local-op
buffer render-dirty-set)
(ebox-incremental--local-reflow-op
buffer render-dirty-set)))))
(setq ops (list coalesced-op))
(setq ops
(mapcar
(lambda (entry)
(ebox-incremental--declarative-tentative-op buffer entry))
render-dirty-set))
(setq ops (ebox-patch-plan-merge-ops parent-table ops))
(setq ops
(ebox-incremental--coalesce-sibling-line-span-ops
buffer ops))))
(ebox-patch-plan-merge-ops parent-table ops)))
(defun ebox-incremental--declarative-tentative-patch-set
(buffer render-dirty-set)
"Return route-selected smallest-owner candidates for RENDER-DIRTY-SET."
(pcase ebox-incremental--patch-planner-route
('legacy
(ebox-incremental--legacy-tentative-patch-set
buffer render-dirty-set))
('pure
(ebox-incremental--pure-tentative-patch-set
buffer render-dirty-set))
('shadow
(let ((legacy
(ebox-incremental--legacy-tentative-patch-set
buffer render-dirty-set))
(pure
(ebox-incremental--pure-tentative-patch-set
buffer render-dirty-set)))
(unless (equal legacy pure)
(signal 'ebox-patch-plan-error
(list :shadow-artifact-mismatch
:legacy legacy :pure pure)))
pure))
(_
(signal 'ebox-patch-plan-error
(list :unknown-route ebox-incremental--patch-planner-route)))))
(defun ebox-incremental--declarative-next-owner-id
(buffer owner-id dirty)
"Return the next ancestor of OWNER-ID worth proving for DIRTY."
(let* ((root-id (ebox--buffer-root-node-id buffer))
(ancestor-id
(or (ebox--runtime-parent-id buffer owner-id) root-id))
(structure-change
(eq (plist-get dirty :dirty-kind) 'structure))
(overflow-coverage
(plist-get dirty :requires-owned-overflow-coverage))
(pure-overflow
(equal (plist-get dirty :changed-keys) '(:overflow))))
(while
(and ancestor-id
(not (equal ancestor-id root-id))
(or
(and structure-change
(not
(ebox-incremental--owner-can-absorb-line-count-change-p
buffer ancestor-id)))
(and overflow-coverage
(or
(not
(ebox--owned-overflow-coverage-wrapper-node-p
buffer ancestor-id))
(and (not pure-overflow)
(ebox--partial-slot-owner-p
buffer ancestor-id))))))
(setq ancestor-id
(or (ebox--runtime-parent-id buffer ancestor-id) root-id)))
ancestor-id))
(defun ebox-incremental--layout-owner-plan
(buffer old-state candidate-state dirty-set)
"Return BUFFER layout-owner plan from OLD-STATE to CANDIDATE-STATE for DIRTY-SET."
(when-let* ((render-dirty-set
(cl-remove-if
(lambda (entry)
(eq (plist-get entry :dirty-kind) 'metadata))
dirty-set)))
(let ((ebox-incremental--buffer-render-state-override
(cons buffer candidate-state))
(ebox-incremental--candidate-base-state
(cons buffer old-state))
(ebox--region-box-table
(plist-get candidate-state :region-box-table))
(ebox--scroll-global-state
(plist-get candidate-state :scroll-state-table))
(ebox--scroll-idle-prefetch-timers
(make-hash-table :test 'equal))
(ebox--smooth-scroll-state-table
(make-hash-table :test 'equal))
(ebox--render-cache-scroll-state-restorable-p nil))
(cl-letf (((symbol-function 'ebox--scroll-schedule-idle-prefetch)
(lambda (&rest _) nil)))
(ebox-incremental--declarative-tentative-patch-set
buffer render-dirty-set)))))
(defun ebox-incremental--cascade-root-owner-plan-p
(old-state candidate-state dirty-set)
"Return non-nil when cascade makes a local proof unusable.
OLD-STATE and CANDIDATE-STATE bound the style evidence for DIRTY-SET. Dynamic
or changed cascade dependencies cannot use a local owner; a static binding
closure may still plan one for a later owner-scoped surface projection."
(let* ((objects (plist-get old-state :surface-node-object-table))
(style-states (plist-get old-state :style-binding-states))
(missing-structure-style-p
(and (hash-table-p objects) (hash-table-p style-states)
(cl-some
(lambda (entry)
(and (eq (plist-get entry :dirty-kind) 'structure)
(when-let* ((object
(gethash (plist-get entry :node-id)
objects)))
(null (gethash object style-states)))))
dirty-set))))
(or
(/= (ebox-tree-author-style-count (plist-get old-state :root-node))
(ebox-tree-author-style-count
(plist-get candidate-state :root-node)))
(and
(or (plist-get old-state :cascade-active-p)
(plist-get old-state :cascade-required-p)
(ebox-style-cascade-active-p))
(cl-some (lambda (entry)
(not (eq (plist-get entry :dirty-kind) 'paint)))
dirty-set)
(or missing-structure-style-p
(not
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state dirty-set)))))))
(defun ebox-incremental--root-owner-plan (buffer dirty-set)
"Return one conservative root owner operation for DIRTY-SET."
(when-let* ((root-id (ebox--buffer-root-node-id buffer)))
(list
(ebox--patch-op
'owner-rerender root-id
:dirty (ebox--merge-dirty-provenance-list dirty-set)
:proof-skipped-p t))))
(defun ebox-incremental--native-frame-plan (buffer dirty-set)
"Return the bounded publication plan for a complete native frame."
(when-let* ((root-id (ebox--buffer-root-node-id buffer)))
(list
(ebox--patch-op
'native-frame root-id
:dirty (ebox--merge-dirty-provenance-list dirty-set)
:proof-skipped-p t))))
(defun ebox-incremental--layout-owner-report
(prepared owner-plan candidate-state)
"Return Ebox report for PREPARED, OWNER-PLAN, and CANDIDATE-STATE."
(let* ((owner-ids
(mapcar (lambda (op) (plist-get op :owner-id)) owner-plan))
(single-owner (and (= (length owner-ids) 1) (car owner-ids)))
(snapshots (plist-get candidate-state :layout-snapshots))
(node-table (plist-get candidate-state :node-table))
(report
(and owner-plan
(ebox--update-report
nil (ebox--patch-results-strategy owner-plan)
:dirty-count (length (plist-get prepared :dirty-set))
:patch-count (length owner-plan)
:patch-ops
(mapcar (lambda (op) (plist-get op :op)) owner-plan)
:owner-ids owner-ids
:owner-type
(plist-get (gethash single-owner node-table) :ebox-type)
:span-count
(cl-loop for owner-id in owner-ids
for snapshot = (and snapshots
(gethash owner-id snapshots))
sum (length (plist-get snapshot :buffer-spans)))
:publication-scope 'layout-owners))))
(ebox-incremental--commit-report prepared report candidate-state)))
(defun ebox-incremental--paint-projection-p
(old-state owner-plan dirty-set)
"Return non-nil when OWNER-PLAN can reuse OLD-STATE spatial topology."
(and owner-plan
(null (plist-get old-state :scroll-region-ids))
(cl-every (lambda (op) (eq (plist-get op :op) 'paint-patch))
owner-plan)
(cl-every
(lambda (dirty)
(cl-every (lambda (key)
(memq key ebox--paint-style-signature-keys))
(plist-get dirty :changed-keys)))
dirty-set)))
(defun ebox-incremental--hash-key-set-equal-p (left right)
"Return non-nil when hash tables LEFT and RIGHT have the same keys."
(and (hash-table-p left)
(hash-table-p right)
(= (hash-table-count left) (hash-table-count right))
(let ((equal-p t)
(missing (make-symbol "ebox-missing-key")))
(maphash
(lambda (key _value)
(when (eq (gethash key right missing) missing)
(setq equal-p nil)))
left)
equal-p)))
(defun ebox-incremental--span-patch-identity-stable-p
(buffer old-state candidate-state prepared owner-id)
"Return non-nil when a span patch preserves candidate identity topology.
The candidate index is already the proof boundary for source identity. This
check verifies its observable sets and the copied path's parent slots before
the surface is allowed to reuse retained TP objects."
(let* ((partitioned-p (plist-get prepared :partitioned-owner-proof-p))
(old-node-table (plist-get old-state :node-table))
(new-node-table (plist-get candidate-state :node-table))
(old-parent-table (plist-get old-state :parent-table))
(new-parent-table (plist-get candidate-state :parent-table))
(touched
(if partitioned-p
(let ((walk owner-id)
(ids
(mapcar (lambda (dirty) (plist-get dirty :node-id))
(plist-get prepared :dirty-set))))
(while walk
(push walk ids)
(setq walk (gethash walk old-parent-table)))
(delete-dups ids))
(delete-dups
(append (plist-get prepared :touched-node-ids)
(list owner-id (ebox--buffer-root-node-id buffer)))))))
(and (or partitioned-p
(ebox-incremental--hash-key-set-equal-p
old-node-table new-node-table))
(or partitioned-p
(ebox-incremental--hash-key-set-equal-p
(plist-get old-state :region-id-set)
(plist-get candidate-state :region-id-set)))
(cl-every
(lambda (node-id)
(let ((old-node (gethash node-id old-node-table))
(new-node (gethash node-id new-node-table)))
(and old-node new-node
(eq (plist-get old-node :ebox-type)
(plist-get new-node :ebox-type))
(equal
(ebox-tree-node-author-key
(plist-get old-state :source-index) old-node)
(ebox-tree-node-author-key
(plist-get candidate-state :source-index) new-node))
(equal (gethash node-id old-parent-table)
(gethash node-id new-parent-table)))))
(delq nil touched)))))
(defun ebox-incremental--path-span-line-count-stable-p
(buffer node spans)
"Return non-nil when path-copied NODE keeps the published line count."
(and (eq (plist-get node :ebox-type) 'box)
(numberp (ebox-get node :width))
(null (ebox-tree-node-children node))
(= (length spans)
(with-current-buffer buffer
(length (ebox-string-lines (ebox--format-content node)))))))
(defun ebox-incremental--layout-slot-style-signature (node)
"Return NODE's layout-significant style, excluding paint-only fields.
Paint changes are validated by the static cascade proof; they must not make
an otherwise unchanged allocated ancestor look geometrically unstable."
(let ((signature (copy-sequence
(ebox-fragment-node-style-signature node))))
(dolist (key ebox--paint-style-signature-keys)
(cl-remf signature key))
signature))
(defun ebox-incremental--cached-layout-snapshot-details (buffer node-id)
"Return transaction-local detailed layout snapshot for NODE-ID."
(let* ((key (cons 'snapshot node-id))
(missing (make-symbol "ebox-snapshot-proof-missing"))
(cached (and ebox-incremental--allocated-slot-proof-cache
(gethash key ebox-incremental--allocated-slot-proof-cache
missing))))
(if (and cached (not (eq cached missing)))
(unless (eq cached 'no-snapshot) cached)
(let ((snapshot (ebox--ensure-layout-snapshot-details buffer node-id)))
(when ebox-incremental--allocated-slot-proof-cache
(puthash key (or snapshot 'no-snapshot)
ebox-incremental--allocated-slot-proof-cache))
snapshot))))
(defun ebox-incremental--cached-allocation-snapshot (buffer node-id)
"Return lightweight allocation evidence for NODE-ID in BUFFER.
This path intentionally omits role topology, overflow, and mount scans; the
final owner output validator still requests the complete snapshot. Allocation
facts are written back to the published snapshot after they have been derived
from the current buffer. They are ordinary derived state, not authority: the
snapshot generation check in `ebox--layout-snapshot' strips them whenever
coordinates are invalidated, and the final owner validator still recomputes
the complete proof."
(let* ((key (cons 'allocation-snapshot node-id))
(missing (make-symbol "ebox-allocation-snapshot-missing"))
(cached (and ebox-incremental--allocated-slot-proof-cache
(gethash key ebox-incremental--allocated-slot-proof-cache
missing))))
(if (and cached (not (eq cached missing)))
(unless (eq cached 'no-snapshot) cached)
(let* ((snapshot (ebox--ensure-layout-snapshot-spans buffer node-id))
(spans (and snapshot (plist-get snapshot :buffer-spans)))
(footprint nil)
(external nil)
(parent-slot nil)
(allocation-shape nil)
(generation
(ebox--buffer-layout-snapshot-detail-generation buffer)))
;; A previous proof may already have materialized these facts.
;; Reusing them avoids rescanning every character on each owner path.
(setq footprint
(or (and snapshot
(plist-get snapshot :span-footprint-signature))
(and spans
(with-current-buffer buffer
(ebox--spans-span-footprint-signature spans)))))
(setq external
(or (and snapshot
(plist-get snapshot :external-footprint-signature))
(and footprint
(ebox--external-footprint-signature-from-span-footprint
footprint))))
(setq parent-slot
(or (and snapshot
(plist-get snapshot :parent-slot-signature))
(and spans
(with-current-buffer buffer
(ebox--spans-parent-slot-signature spans)))))
(setq allocation-shape
(and footprint
(list :span-count (plist-get footprint :span-count)
:whole-line-flags
(plist-get footprint :whole-line-flags)
:contiguous-lines
(plist-get footprint :contiguous-lines))))
(let ((allocation
(and footprint
(list :buffer-spans spans
:span-footprint-signature footprint
:external-footprint-signature external
:parent-slot-signature parent-slot
:allocation-shape allocation-shape
:detail-generation generation))))
;; Keep the allocation facts with the snapshot table so the next
;; logical turn can consume them without repeating the scan. These
;; are derived facts only; publication authority stays unchanged.
(when (and snapshot allocation
(or (null (plist-get snapshot
:span-footprint-signature))
(null (plist-get snapshot
:external-footprint-signature))
(null (plist-get snapshot
:parent-slot-signature))
(null (plist-get snapshot :allocation-shape))
(not (= (or (plist-get snapshot
:detail-generation)
-1)
generation))))
(let ((updated (copy-sequence snapshot)))
(dolist (entry `((:span-footprint-signature . ,footprint)
(:external-footprint-signature . ,external)
(:parent-slot-signature . ,parent-slot)
(:allocation-shape . ,allocation-shape)
(:detail-generation . ,generation)))
(setq updated (plist-put updated (car entry) (cdr entry))))
(ebox--put-layout-snapshot buffer node-id updated)))
(when ebox-incremental--allocated-slot-proof-cache
(puthash key (or allocation 'no-snapshot)
ebox-incremental--allocated-slot-proof-cache))
allocation)))))
(defun ebox-incremental--retained-ancestor-slot-signature
(old-state candidate-state owner-id)
"Return a still-valid published allocation certificate for OWNER-ID."
(let* ((certificates
(plist-get old-state :retained-allocation-certificates))
(certificate
(and (hash-table-p certificates)
(gethash owner-id certificates)))
(signature (and certificate (plist-get certificate :signature)))
(old-nodes (plist-get old-state :node-table))
(new-nodes (plist-get candidate-state :node-table))
(old-parents (plist-get old-state :parent-table))
(new-parents (plist-get candidate-state :parent-table))
(old-source-index (plist-get old-state :source-index))
(new-source-index (plist-get candidate-state :source-index))
(entries signature)
(valid (and signature
(equal (plist-get certificate :root-id)
(plist-get (plist-get old-state :root-node)
:node-id))
(equal (plist-get certificate :root-id)
(plist-get (plist-get candidate-state :root-node)
:node-id))
(equal (plist-get certificate :viewport-width)
(plist-get old-state :viewport-width))
(equal (plist-get certificate :viewport-height)
(plist-get old-state :viewport-height))
(equal (plist-get certificate :display-signature)
(plist-get old-state :display-signature))
(equal (plist-get old-state :display-signature)
(plist-get candidate-state :display-signature)))))
(while (and valid entries)
(let* ((entry (car entries))
(node-id (plist-get entry :node-id))
(next-id (and (cdr entries)
(plist-get (cadr entries) :node-id)))
(old-node (and node-id (gethash node-id old-nodes)))
(new-node (and node-id (gethash node-id new-nodes)))
(old-parent (and node-id (gethash node-id old-parents)))
(new-parent (and node-id (gethash node-id new-parents)))
(allocated-style (plist-get entry :allocated-style)))
(setq valid
(and old-node new-node allocated-style
(equal old-parent new-parent)
(or (null next-id) (equal old-parent next-id))
(eq (plist-get old-node :ebox-type)
(plist-get new-node :ebox-type))
(equal (ebox-tree-node-key old-source-index old-node)
(ebox-tree-node-key new-source-index new-node))
;; This certificate describes OWNER-ID's allocation in its
;; parent, not the contents it allocates internally. A
;; Range owner is expected to replace its children while
;; retaining that external slot; the final rendered-span
;; validator still proves its exact published footprint.
;; Ancestors above the owner must retain child topology
;; because it defines the certified sibling allocation.
(or (equal node-id owner-id)
(ebox-incremental--node-child-ids-equal-p
old-node new-node))
(equal allocated-style
(ebox-incremental--layout-slot-style-signature
old-node))
(equal allocated-style
(ebox-incremental--layout-slot-style-signature
new-node))
(or (not (equal node-id owner-id))
(and (equal (ebox-get old-node :width)
(ebox-get new-node :width))
(equal (ebox-get old-node :height)
(ebox-get new-node :height)))))))
(setq entries (cdr entries)))
(and valid (copy-tree signature))))
(defun ebox-incremental--compute-ancestor-slot-signature
(buffer old-state candidate-state owner-id)
"Return stable allocated-slot evidence from OWNER-ID through the root.
Every retained ancestor must keep its parent edge and computed layout style.
Published snapshot footprints make the allocation proof independent of the
candidate's unrendered caches."
(let ((old-nodes (plist-get old-state :node-table))
(new-nodes (plist-get candidate-state :node-table))
(old-parents (plist-get old-state :parent-table))
(new-parents (plist-get candidate-state :parent-table))
(generation (plist-get old-state :layout-snapshot-detail-generation))
(node-id owner-id)
signature valid)
(setq valid t)
(while (and valid node-id)
(let* ((old-node (gethash node-id old-nodes))
(new-node (gethash node-id new-nodes))
(old-parent (gethash node-id old-parents))
(new-parent (gethash node-id new-parents))
(snapshot (and old-node
(ebox-incremental--cached-allocation-snapshot
buffer node-id)))
(old-style (and old-node
(ebox-incremental--layout-slot-style-signature
old-node)))
(new-style (and new-node
(ebox-incremental--layout-slot-style-signature
new-node))))
(if (and old-node new-node snapshot
(equal old-parent new-parent)
(if (equal node-id owner-id)
(and (equal (ebox-get old-node :width)
(ebox-get new-node :width))
(equal (ebox-get old-node :height)
(ebox-get new-node :height)))
(equal old-style new-style))
(plist-get snapshot :span-footprint-signature)
(plist-get snapshot :external-footprint-signature)
(or (null old-parent)
(plist-get snapshot :parent-slot-signature)))
(push (list :node-id node-id
:allocated-style old-style
:parent-slot-signature
(plist-get snapshot :parent-slot-signature)
:external-footprint-signature
(plist-get snapshot :external-footprint-signature)
:span-footprint-signature
(plist-get snapshot :span-footprint-signature)
:detail-generation generation)
signature)
(setq valid nil))
(setq node-id old-parent)))
(and valid (nreverse signature))))
(defun ebox-incremental--ancestor-slot-signature
(buffer old-state candidate-state owner-id)
"Return candidate-local cached ancestor allocation evidence for OWNER-ID."
(let* ((key (cons 'ancestor owner-id))
(missing (make-symbol "ebox-ancestor-proof-missing"))
(cached (and ebox-incremental--allocated-slot-proof-cache
(gethash key ebox-incremental--allocated-slot-proof-cache
missing))))
(if (and cached (not (eq cached missing)))
(unless (eq cached 'no-signature) cached)
(let ((signature
(or (ebox-incremental--retained-ancestor-slot-signature
old-state candidate-state owner-id)
(ebox-incremental--compute-ancestor-slot-signature
buffer old-state candidate-state owner-id))))
(when ebox-incremental--allocated-slot-proof-cache
(puthash key (or signature 'no-signature)
ebox-incremental--allocated-slot-proof-cache))
signature))))
(defun ebox-incremental--stable-grid-track-p (track)
"Return non-nil when TRACK is fixed or a positive fractional track."
(or (numberp track)
(and (consp track) (null (cdr track)) (numberp (car track)))
(and (proper-list-p track)
(memq (plist-get track :kind) '(fixed fr))
(let ((value (if (eq (plist-get track :kind) 'fixed)
(plist-get track :size)
(plist-get track :factor))))
(and (numberp value) (>= value 0)
(or (eq (plist-get track :kind) 'fixed)
(> value 0)))))
(and (symbolp track)
(string-match-p
"\\`[0-9]+\\(?:\\.[0-9]+\\)?fr\\'" (symbol-name track)))
(and (consp track) (eq (car track) 'fr)
(numberp (cadr track)) (> (cadr track) 0)
(null (cddr track)))))
(defun ebox-incremental--fixed-grid-track-p (track)
"Return non-nil when TRACK has a definite numeric size.
The fast variable-content proof deliberately excludes fractional tracks; their
allocation remains on the full ancestor-slot proof path."
(or (numberp track)
(and (consp track) (null (cdr track)) (numberp (car track)))
(and (proper-list-p track)
(eq (plist-get track :kind) 'fixed)
(numberp (plist-get track :size)))))
(defun ebox-incremental--compute-allocated-parent-slot-stable-p
(buffer entry candidate-state)
"Return non-nil when ENTRY keeps a fixed parent allocation in CANDIDATE-STATE.
This bounded proof accepts an auto-width one-line child only when its nearest
unchanged Grid ancestor uses fixed or positive fractional columns and the
complete published ancestor slot chain remains identical."
(let* ((old-state (ebox-incremental--proof-base-state buffer))
(node-id (plist-get entry :node-id))
(old-node (and node-id
(gethash node-id (plist-get old-state :node-table))))
(new-node (and node-id
(gethash node-id
(plist-get candidate-state :node-table))))
(parent-table (plist-get old-state :parent-table))
(candidate-parents (plist-get candidate-state :parent-table))
(ancestor-id (and node-id (gethash node-id parent-table)))
grid-id old-grid new-grid)
(while (and ancestor-id (not grid-id))
(let ((old-ancestor
(gethash ancestor-id (plist-get old-state :node-table)))
(new-ancestor
(gethash ancestor-id (plist-get candidate-state :node-table))))
(if (and old-ancestor new-ancestor
(equal (gethash ancestor-id parent-table)
(gethash ancestor-id candidate-parents))
(eq (ebox-tree-display-inner old-ancestor) 'grid)
(eq (ebox-tree-display-inner new-ancestor) 'grid))
(setq grid-id ancestor-id
old-grid old-ancestor
new-grid new-ancestor)
(setq ancestor-id (gethash ancestor-id parent-table)))))
(let* ((old-props (and old-grid (ebox-tree-layout-props old-grid)))
(new-props (and new-grid (ebox-tree-layout-props new-grid)))
(tracks (and old-props
(plist-get old-props :grid-template-columns)))
(owner-snapshot
(and old-node
(ebox-incremental--cached-allocation-snapshot
buffer node-id)))
(old-content (plist-get (plist-get entry :old-signature) :content))
(new-content (plist-get (plist-get entry :new-signature) :content)))
(and old-state old-node new-node old-grid new-grid
(memq :content (plist-get entry :changed-keys))
(or (and (numberp (ebox-get old-node :height))
(equal (ebox-get old-node :height)
(ebox-get new-node :height)))
(and (equal (plist-get
(plist-get owner-snapshot
:external-footprint-signature)
:block-lines)
1)
(stringp old-content) (stringp new-content)
(not (string-match-p "\n" old-content))
(not (string-match-p "\n" new-content))))
(equal old-props new-props)
(proper-list-p tracks)
tracks
(cl-every #'ebox-incremental--stable-grid-track-p tracks)
(ebox-incremental--ancestor-slot-signature
buffer old-state candidate-state node-id)))))
(defun ebox-incremental--compute-content-slot-stable
(buffer entry candidate-state)
"Return strict evidence that a one-line auto-width owner is local.
The owner may have no declared width when its nearest unchanged formatting
context has a stable allocated slot. A column parent is safe when its own
layout facts and child topology are unchanged; a row parent is accepted only
for the final child, so changing that child's intrinsic width cannot shift a
following sibling. The surface footprint validator remains the final gate."
(let* ((old-state (ebox-incremental--proof-base-state buffer))
(node-id (plist-get entry :node-id))
(old-node (and node-id
(gethash node-id (plist-get old-state :node-table))))
(new-node (and node-id
(gethash node-id (plist-get candidate-state :node-table))))
(parents (plist-get old-state :parent-table))
(candidate-parents (plist-get candidate-state :parent-table))
(parent-id (and node-id (gethash node-id parents)))
(old-parent (and parent-id
(gethash parent-id (plist-get old-state :node-table))))
(new-parent (and parent-id
(gethash parent-id (plist-get candidate-state :node-table))))
(old-children (and old-parent (ebox-tree--children-raw old-parent)))
(position (and old-children
(cl-position-if
(lambda (child)
(= node-id (plist-get child :node-id)))
old-children)))
(parent-kind (and old-parent (ebox-tree-display-inner old-parent)))
(snapshot (and old-node
(ebox-incremental--cached-allocation-snapshot
buffer node-id)))
(old-content (plist-get (plist-get entry :old-signature) :content))
(new-content (plist-get (plist-get entry :new-signature) :content))
(ancestor-signature
(and node-id
(ebox-incremental--ancestor-slot-signature
buffer old-state candidate-state node-id))))
(and old-state old-node new-node old-parent new-parent
(equal (gethash parent-id parents)
(gethash parent-id candidate-parents))
(ebox-incremental--node-child-ids-equal-p old-parent new-parent)
(equal (ebox-incremental--layout-slot-style-signature old-parent)
(ebox-incremental--layout-slot-style-signature new-parent))
(equal (ebox-get old-parent :width) (ebox-get new-parent :width))
(equal (ebox-get old-parent :height) (ebox-get new-parent :height))
(memq parent-kind '(row column))
(or (eq parent-kind 'column)
(and (integerp position)
(= position (1- (length old-children)))
(not (ebox-get old-node :flex-grow))
(not (ebox-get old-node :flex-shrink))))
(memq :content (plist-get entry :changed-keys))
(stringp old-content) (stringp new-content)
(not (string-match-p "\n" old-content))
(not (string-match-p "\n" new-content))
snapshot
(equal (plist-get
(plist-get snapshot :external-footprint-signature)
:block-lines)
1)
ancestor-signature)))
(defun ebox-incremental--content-slot-stable-p
(buffer entry candidate-state)
"Return cached one-line content-slot evidence for ENTRY."
(if (not ebox-incremental--allocated-slot-proof-cache)
(ebox-incremental--compute-content-slot-stable
buffer entry candidate-state)
(let* ((old-signature (plist-get entry :old-signature))
(new-signature (plist-get entry :new-signature))
(key (list 'content-slot (plist-get entry :node-id)
(plist-get entry :changed-keys)
(plist-get old-signature :content)
(plist-get new-signature :content)))
(missing (make-symbol "ebox-content-slot-proof-missing"))
(cached (gethash key ebox-incremental--allocated-slot-proof-cache
missing)))
(if (not (eq cached missing))
(unless (eq cached 'no-evidence) cached)
(let ((evidence
(ebox-incremental--compute-content-slot-stable
buffer entry candidate-state)))
(puthash key (or evidence 'no-evidence)
ebox-incremental--allocated-slot-proof-cache)
evidence)))))
(defun ebox-incremental--owner-slot-stable-p
(buffer old-state candidate-state owner-id)
"Return OWNER-ID's retained parent-slot certificate, or nil.
The owner and its parent must retain identity, child topology, layout style,
and a normal Column allocation. Candidate rendering remains responsible for
proving that changed descendants still fit this slot exactly."
(let* ((old-nodes (plist-get old-state :node-table))
(new-nodes (plist-get candidate-state :node-table))
(old-parents (plist-get old-state :parent-table))
(new-parents (plist-get candidate-state :parent-table))
(old-node (and owner-id (gethash owner-id old-nodes)))
(new-node (and owner-id (gethash owner-id new-nodes)))
(parent-id (and owner-id (gethash owner-id old-parents)))
(old-parent (and parent-id (gethash parent-id old-nodes)))
(new-parent (and parent-id (gethash parent-id new-nodes)))
(snapshot
(and old-node
(ebox-incremental--cached-allocation-snapshot
buffer owner-id))))
(and old-node new-node old-parent new-parent snapshot
(equal parent-id (gethash owner-id new-parents))
(eq (ebox-tree-display-inner old-parent) 'column)
(eq (ebox-tree-display-inner new-parent) 'column)
(ebox-incremental--node-child-ids-equal-p old-node new-node)
(ebox-incremental--node-child-ids-equal-p old-parent new-parent)
(equal (ebox-incremental--layout-slot-style-signature old-node)
(ebox-incremental--layout-slot-style-signature new-node))
(equal (ebox-incremental--layout-slot-style-signature old-parent)
(ebox-incremental--layout-slot-style-signature new-parent))
(not (ebox-tree-node-visible-overflow-p old-node))
(not (ebox-tree-node-visible-overflow-p new-node))
(ebox-incremental--ancestor-slot-signature
buffer old-state candidate-state owner-id))))
(defun ebox-incremental--allocated-parent-slot-stable-p
(buffer entry candidate-state)
"Return cached allocated-slot evidence for ENTRY in CANDIDATE-STATE."
(let* ((key (cons 'allocated (plist-get entry :node-id)))
(missing (make-symbol "ebox-slot-proof-missing"))
(cached (and ebox-incremental--allocated-slot-proof-cache
(gethash key ebox-incremental--allocated-slot-proof-cache
missing))))
(if (and cached (not (eq cached missing)))
(eq cached 'stable)
(let ((stable
(ebox-incremental--compute-allocated-parent-slot-stable-p
buffer entry candidate-state)))
(when ebox-incremental--allocated-slot-proof-cache
(puthash key (if stable 'stable 'unstable)
ebox-incremental--allocated-slot-proof-cache))
stable))))
(defun ebox-incremental--fixed-content-slot-stable-p
(buffer entry candidate-state)
"Return strict evidence for one-line content in a definite flex/row slot.
Unlike the auto-width row proof, a definite non-growing child cannot change
its parent's allocation when its glyph string changes. Parent topology,
style, and the published ancestor slot chain remain mandatory."
(let* ((old-state (ebox-incremental--proof-base-state buffer))
(node-id (plist-get entry :node-id))
(old-node (and node-id
(gethash node-id (plist-get old-state :node-table))))
(new-node (and node-id
(gethash node-id (plist-get candidate-state
:node-table))))
(parents (plist-get old-state :parent-table))
(candidate-parents (plist-get candidate-state :parent-table))
(parent-id (and node-id (gethash node-id parents)))
(old-parent (and parent-id
(gethash parent-id (plist-get old-state :node-table))))
(new-parent (and parent-id
(gethash parent-id
(plist-get candidate-state :node-table))))
(snapshot (and old-node
(ebox-incremental--cached-allocation-snapshot
buffer node-id)))
(old-content (plist-get (plist-get entry :old-signature) :content))
(new-content (plist-get (plist-get entry :new-signature) :content)))
(and old-node new-node old-parent new-parent
(eq (ebox-tree-display-inner old-parent)
(ebox-tree-display-inner new-parent))
(memq (ebox-tree-display-inner old-parent) '(row flex))
(equal (gethash parent-id parents)
(gethash parent-id candidate-parents))
(ebox-incremental--node-child-ids-equal-p old-parent new-parent)
(equal (ebox-incremental--layout-slot-style-signature old-parent)
(ebox-incremental--layout-slot-style-signature new-parent))
(numberp (ebox-get old-node :width))
(equal (ebox-get old-node :width) (ebox-get new-node :width))
(not (ebox-get old-node :flex-grow))
(not (ebox-get old-node :flex-shrink))
(memq :content (plist-get entry :changed-keys))
(stringp old-content) (stringp new-content)
(not (string-match-p "\n" old-content))
(not (string-match-p "\n" new-content))
snapshot
(equal (plist-get
(plist-get snapshot :external-footprint-signature)
:block-lines)
1)
(ebox-incremental--ancestor-slot-signature
buffer old-state candidate-state node-id))))
(defun ebox-incremental--stable-grid-ancestor-slot-proof-p
(old-state candidate-state grid-id)
"Return non-nil for a stable fractional GRID allocation context.
This proof is deliberately narrower than a general ancestor snapshot proof:
GRID must be nested below at least one unchanged normal-flow ancestor, every
ancestor above it must remain a column/box flow with the same layout style and
parent edge, and no ancestor may expose visible overflow. Auto-sized
formatting contexts, row/flex/grid ancestors, and a root GRID continue through
the complete snapshot proof."
(let* ((old-nodes (plist-get old-state :node-table))
(new-nodes (plist-get candidate-state :node-table))
(old-parents (plist-get old-state :parent-table))
(new-parents (plist-get candidate-state :parent-table))
(node-id (gethash grid-id old-parents))
(seen nil)
(valid (and node-id t)))
(while (and valid node-id)
(if (memq node-id seen)
(setq valid nil)
(push node-id seen)
(let* ((old-node (gethash node-id old-nodes))
(new-node (gethash node-id new-nodes))
(old-parent (gethash node-id old-parents))
(new-parent (gethash node-id new-parents))
(display (and old-node
(ebox-tree-display-inner old-node)))
(old-width (and old-node (ebox-get old-node :width)))
(new-width (and new-node (ebox-get new-node :width))))
(unless (and old-node new-node
(equal old-parent new-parent)
(equal (ebox-incremental--layout-slot-style-signature
old-node)
(ebox-incremental--layout-slot-style-signature
new-node))
(memq display '(column box flow))
(not (ebox-tree-node-visible-overflow-p old-node))
(not (ebox-tree-node-visible-overflow-p new-node))
;; A normal-flow column/box may inherit its width from
;; its stable parent. Intrinsic width keywords are not
;; allocation certificates and remain conservative.
(equal old-width new-width)
(not (memq old-width '(max-content min-content
fit-content auto))))
(setq valid nil))
(setq node-id old-parent))))
valid))
(defun ebox-incremental--compute-fast-grid-slot-evidence
(buffer entry candidate-state)
"Return minimal stable Grid-slot evidence for one variable owner.
This intentionally stops at the nearest unchanged Grid instead of rebuilding
every ancestor snapshot. The full proof remains available for all other
projection kinds and the surface output validator remains authoritative."
(let* ((old-state (ebox-incremental--proof-base-state buffer))
(node-id (plist-get entry :node-id))
(old-node (and node-id
(gethash node-id (plist-get old-state :node-table))))
(new-node (and node-id
(gethash node-id
(plist-get candidate-state :node-table))))
(parents (plist-get old-state :parent-table))
(candidate-parents (plist-get candidate-state :parent-table))
(ancestor-id (and node-id (gethash node-id parents)))
(child-id node-id)
grid-id old-grid new-grid)
(while (and ancestor-id (not grid-id))
(let* ((old-ancestor (gethash ancestor-id
(plist-get old-state :node-table)))
(new-ancestor (gethash ancestor-id
(plist-get candidate-state :node-table)))
(parent-id (gethash ancestor-id parents))
(new-parent-id (gethash ancestor-id candidate-parents)))
(if (and old-ancestor new-ancestor
(equal parent-id new-parent-id)
(not (ebox-tree-node-visible-overflow-p old-ancestor)))
(pcase (ebox-tree-display-inner old-ancestor)
('grid
(setq grid-id ancestor-id
old-grid old-ancestor
new-grid new-ancestor))
('flex
(let* ((children (ebox-tree--children-raw old-ancestor))
(last-child (car (last children)))
(props (plist-get old-ancestor :props)))
(if (and last-child
(= child-id (plist-get last-child :node-id))
(eq (plist-get props :flex-wrap) 'nowrap)
(not (ebox-get last-child :flex-grow))
(not (ebox-get last-child :flex-shrink))
(equal (ebox-incremental--layout-slot-style-signature
old-ancestor)
(ebox-incremental--layout-slot-style-signature
new-ancestor)))
(setq grid-id ancestor-id
old-grid old-ancestor
new-grid new-ancestor
ancestor-id nil)
(setq ancestor-id nil))))
(_
(setq child-id ancestor-id
ancestor-id parent-id)))
(setq ancestor-id nil))))
(let* ((old-props (and old-grid (ebox-tree-layout-props old-grid)))
(new-props (and new-grid (ebox-tree-layout-props new-grid)))
(context-kind (and old-grid (ebox-tree-display-inner old-grid)))
(tracks (and old-props
(plist-get old-props :grid-template-columns)))
(snapshot (and old-node
(ebox-incremental--cached-allocation-snapshot
buffer node-id)))
(grid-snapshot (and grid-id
(ebox-incremental--cached-allocation-snapshot
buffer grid-id)))
(old-content
(plist-get (plist-get entry :old-signature) :content))
(new-content
(plist-get (plist-get entry :new-signature) :content)))
(when (and old-node new-node old-grid new-grid
(equal old-props new-props)
(or (eq context-kind 'flex)
(and (eq context-kind 'grid)
(proper-list-p tracks) tracks
(cl-every #'ebox-incremental--stable-grid-track-p
tracks)))
snapshot grid-snapshot
(= 1 (plist-get
(plist-get snapshot :external-footprint-signature)
:block-lines))
(stringp old-content) (stringp new-content)
(not (string-match-p "\n" old-content))
(not (string-match-p "\n" new-content)))
(list :owner-id node-id :grid-id grid-id
:needs-full-ancestor-proof
(and (eq context-kind 'grid)
(not (cl-every #'ebox-incremental--fixed-grid-track-p
tracks))
(not (ebox-incremental--stable-grid-ancestor-slot-proof-p
old-state candidate-state grid-id)))
:grid-snapshot grid-snapshot
:owner-snapshot snapshot)))))
(defun ebox-incremental--fast-grid-slot-evidence
(buffer entry candidate-state)
"Return cached minimal Grid-slot evidence for ENTRY in CANDIDATE-STATE."
(if (not ebox-incremental--allocated-slot-proof-cache)
(ebox-incremental--compute-fast-grid-slot-evidence
buffer entry candidate-state)
(let* ((old-signature (plist-get entry :old-signature))
(new-signature (plist-get entry :new-signature))
(key (list 'fast-grid (plist-get entry :node-id)
(plist-get entry :changed-keys)
(plist-get old-signature :content)
(plist-get new-signature :content)))
(missing (make-symbol "ebox-fast-grid-proof-missing"))
(cached (gethash key ebox-incremental--allocated-slot-proof-cache
missing)))
(if (not (eq cached missing))
(unless (eq cached 'no-evidence) cached)
(let ((evidence
(ebox-incremental--compute-fast-grid-slot-evidence
buffer entry candidate-state)))
(puthash key (or evidence 'no-evidence)
ebox-incremental--allocated-slot-proof-cache)
evidence)))))
(defun ebox-incremental--variable-content-fits-slot-p
(snapshot content)
"Return non-nil when one-line CONTENT fits the retained owner slot.
This is a conservative pre-render guard for variable-content span patches;
the surface renderer and role/overflow validators remain the final proof."
(let ((slots (plist-get (plist-get snapshot :parent-slot-signature)
:slots)))
(and (stringp content)
(not (string-match-p "\n" content))
slots
(<= (ebox--string-pixel-width content)
(apply #'min
(mapcar (lambda (slot)
(- (plist-get slot :end-pixel)
(plist-get slot :start-pixel)))
slots))))))
(defun ebox-incremental--single-span-patch-projection-proof
(buffer old-state prepared candidate-state owner-plan
&optional path-copied-p)
"Return strict span projection metadata, or nil when it is unsafe.
This first slice deliberately accepts only one fixed-footprint content owner.
The surface still performs the output and text-property proof before using the
metadata; this predicate only authorizes the local attempt."
(when (and (= (length owner-plan) 1)
(= (length (plist-get prepared :dirty-set)) 1))
(let* ((operation (car owner-plan))
(dirty (car (plist-get prepared :dirty-set)))
(owner-id (plist-get operation :owner-id))
(changed-keys (plist-get dirty :changed-keys))
(old-node (gethash owner-id (plist-get old-state :node-table)))
(new-node (gethash owner-id (plist-get candidate-state :node-table)))
(slot-dirty
(plist-put (copy-sequence dirty) :node-id owner-id))
(snapshot
(and old-node
(ebox-incremental--cached-layout-snapshot-details
buffer owner-id)))
(variable-grid-evidence
(and (memq :content changed-keys)
(ebox-incremental--fast-grid-slot-evidence
buffer slot-dirty candidate-state)))
(fixed-content-evidence
(and (memq :content changed-keys)
(ebox-incremental--fixed-content-slot-stable-p
buffer slot-dirty candidate-state)))
(content-slot-evidence
(and (memq :content changed-keys)
(ebox-incremental--content-slot-stable-p
buffer slot-dirty candidate-state)))
(local-typography-p
(and (not (memq :content changed-keys))
(cl-some
(lambda (key)
(memq key ebox--typography-style-signature-keys))
changed-keys)
(cl-every
(lambda (key)
(or (memq key ebox--paint-style-signature-keys)
(memq key ebox--typography-style-signature-keys)))
changed-keys)))
(fixed-flex-evidence
(and (memq :content changed-keys)
(ebox--flex-item-fixed-basis-content-allocation-stable-p
buffer owner-id)))
(spans (and snapshot (plist-get snapshot :buffer-spans)))
(impact-vector
(let ((ebox-incremental--candidate-state-for-slot-proof
candidate-state))
(plist-get
(ebox--dirty-entry-with-impact-vector buffer dirty)
:impact-vector)))
(ancestor-slot-signature
(if (and variable-grid-evidence
(not (plist-get variable-grid-evidence
:needs-full-ancestor-proof)))
(list (list :node-id owner-id
:variable-grid-id
(plist-get variable-grid-evidence :grid-id)
:parent-slot-signature
(plist-get snapshot :parent-slot-signature)
:external-footprint-signature
(plist-get snapshot
:external-footprint-signature)
:span-footprint-signature
(plist-get snapshot :span-footprint-signature)
:detail-generation
(plist-get old-state
:layout-snapshot-detail-generation)))
(or content-slot-evidence
(ebox-incremental--ancestor-slot-signature
buffer old-state candidate-state owner-id))))
(allocated-width
(let* ((owner-slot
(if content-slot-evidence
(or (cadr content-slot-evidence)
(car content-slot-evidence))
(car ancestor-slot-signature)))
(slots (plist-get
(plist-get owner-slot :parent-slot-signature)
:slots))
(widths
(mapcar
(lambda (slot)
(- (plist-get slot :end-pixel)
(plist-get slot :start-pixel)))
slots)))
(and widths (apply #'min widths)))))
(when (and (or (eq (plist-get operation :op) 'span-patch)
(and (eq (plist-get operation :op) 'owner-rerender)
(or local-typography-p
(and (memq :content changed-keys)
variable-grid-evidence))))
(eq (plist-get dirty :dirty-kind) 'geometry)
;; A declarative content update may also carry paint keys
;; (for example a stylesheet-computed :color) in the dirty
;; signature. The static cascade proof below establishes
;; that those extra keys cannot affect geometry. Other
;; local style keys remain on their existing conservative
;; path.
(or (memq :content changed-keys) local-typography-p)
(cl-every
(lambda (key)
(or (eq key :content)
(eq key :surface-properties)
(memq key ebox--paint-style-signature-keys)
(memq key ebox--typography-style-signature-keys)))
changed-keys)
(not (plist-get dirty :children))
(null (plist-get dirty :node-ids))
(or local-typography-p
(not (memq 'parent-layout impact-vector))
content-slot-evidence
fixed-content-evidence
fixed-flex-evidence
variable-grid-evidence
(ebox-incremental--allocated-parent-slot-stable-p
buffer slot-dirty candidate-state))
ancestor-slot-signature
(or (and (not (plist-get old-state :cascade-active-p))
(not (plist-get old-state :cascade-required-p))
(not (ebox-style-cascade-active-p)))
(and (or (memq :content changed-keys)
local-typography-p)
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state (list dirty))))
(null (plist-get old-state :scroll-region-ids))
(or (null (plist-get candidate-state :scroll-region-ids))
(null (ebox-incremental--hash-keys
(plist-get candidate-state :scroll-state-table))))
;; The ancestor allocation proof is tied to the published
;; viewport slot. A declarative commit does not alter that
;; context; final footprint validation still rejects a miss.
old-node new-node
(eq (plist-get old-node :ebox-type)
(plist-get new-node :ebox-type))
(equal
(ebox-tree-node-author-key
(plist-get old-state :source-index) old-node)
(ebox-tree-node-author-key
(plist-get candidate-state :source-index) new-node))
(ebox-incremental--node-child-ids-equal-p
old-node new-node)
(or local-typography-p
(equal (ebox-incremental--layout-slot-style-signature
old-node)
(ebox-incremental--layout-slot-style-signature
new-node)))
(ebox-incremental--same-root-surface-identity-p
old-state candidate-state)
(ebox-incremental--span-patch-identity-stable-p
buffer old-state candidate-state prepared owner-id)
(ebox--layout-snapshot-detailed-p snapshot)
spans
(or local-typography-p
(not path-copied-p)
(ebox-incremental--path-span-line-count-stable-p
buffer new-node spans)
content-slot-evidence
fixed-flex-evidence
variable-grid-evidence
(ebox-incremental--allocated-parent-slot-stable-p
buffer slot-dirty candidate-state))
(plist-get snapshot :span-footprint-signature)
(plist-get snapshot :external-footprint-signature)
(plist-get snapshot :parent-slot-signature)
(plist-get snapshot :role-topology-signature)
(plist-get snapshot :overflow-signature)
;; Partial row slots are disjoint by design; the backend
;; shapes each slot independently before the replacement.
(with-current-buffer buffer
(or (ebox-buffer--partial-line-slots-p spans)
(ebox--spans-contiguous-lines-p spans)))
(not (ebox--node-visible-overflow-p old-node))
(not (ebox--ancestor-own-overflow-visible-p buffer owner-id)))
(list :owner-id owner-id :dirty-set (list dirty)
:snapshot snapshot
:ancestor-slot-signature ancestor-slot-signature
:variable-content-p
(and snapshot
(memq :content changed-keys)
(stringp
(plist-get (plist-get dirty :new-signature) :content))
(not (string-match-p
"\n"
(plist-get (plist-get dirty :new-signature)
:content)))
(or variable-grid-evidence fixed-content-evidence
fixed-flex-evidence content-slot-evidence))
:variable-content-max-width
(or (and content-slot-evidence allocated-width)
(and snapshot
(plist-get
(plist-get snapshot :external-footprint-signature)
:max-line-pixel-width)))
:allocated-width allocated-width
:layout-snapshot-detail-generation
(plist-get old-state :layout-snapshot-detail-generation)
:static-cascade-p
(and (or (memq :content changed-keys) local-typography-p)
(or (plist-get old-state :cascade-active-p)
(plist-get old-state :cascade-required-p))
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state (list dirty))))))))
(defun ebox-incremental--owner-proofs-disjoint-p (buffer proofs)
"Return non-nil when PROOFS own pairwise-disjoint published spans."
(let ((spans
(sort
(cl-mapcan
(lambda (proof)
(copy-sequence
(plist-get
(ebox-incremental--cached-layout-snapshot-details
buffer (plist-get proof :owner-id))
:buffer-spans)))
proofs)
(lambda (left right) (< (car left) (car right)))))
valid previous-end)
(setq valid t)
(dolist (span spans)
(when (and previous-end (< (car span) previous-end))
(setq valid nil))
(setq previous-end (cdr span)))
valid))
(defun ebox-incremental--variable-span-proofs-safe-p
(_buffer proofs)
"Return non-nil when every variable owner fits its retained slot capacity."
(let (valid)
(setq valid t)
(dolist (proof proofs)
(when (and valid (plist-get proof :variable-content-p))
(let ((snapshot (plist-get proof :snapshot))
(allocated-width (plist-get proof :allocated-width)))
(dolist (dirty (plist-get proof :dirty-set))
(when valid
(let ((content (plist-get (plist-get dirty :new-signature)
:content)))
(when (and (stringp content)
(not
(if (and (numberp allocated-width)
(not (string-match-p "\n" content)))
(<= (ebox--string-pixel-width content)
allocated-width)
(ebox-incremental--variable-content-fits-slot-p
snapshot content))))
(setq valid nil))))))))
valid))
(defun ebox-incremental--grouped-owner-span-proof
(buffer old-state prepared candidate-state owner-plan)
"Return a single-owner proof for grouped descendants in one stable slot.
The layout owner plan may supply either a content-independent fixed Flex basis
or a retained content-slot certificate. The surface still renders the whole
candidate owner into its published slot and verifies exact footprint, overflow,
role, and outside-complement compatibility before publication."
(when (and (= (length owner-plan) 1)
(> (length (plist-get prepared :dirty-set)) 1))
(let* ((operation (car owner-plan))
(owner-id (plist-get operation :owner-id))
(dirty-set (plist-get prepared :dirty-set))
(root-id (ebox--buffer-root-node-id buffer))
(old-node (gethash owner-id (plist-get old-state :node-table)))
(new-node (gethash owner-id
(plist-get candidate-state :node-table)))
(fixed-flex-slot-p
(ebox--flex-item-fixed-basis-content-allocation-stable-p
buffer owner-id))
(snapshot
(and old-node
(if fixed-flex-slot-p
(ebox-incremental--cached-allocation-snapshot
buffer owner-id)
(ebox-incremental--cached-layout-snapshot-details
buffer owner-id))))
(changed-keys
(delete-dups
(cl-mapcan
(lambda (entry)
(copy-sequence (plist-get entry :changed-keys)))
dirty-set)))
(fixed-flex-ancestor-proof
(and fixed-flex-slot-p
(ebox-incremental--ancestor-slot-signature
buffer old-state candidate-state owner-id)))
(owner-slot-proof
(and (not fixed-flex-ancestor-proof)
(ebox-incremental--owner-slot-stable-p
buffer old-state candidate-state owner-id)))
;; A grouped patch publishes the owner as one unit. Its retained
;; external allocation is therefore the canonical certificate;
;; probing each changed leaf repeats the same ancestor walk and
;; produces a certificate for an identity that is not published as
;; the group owner. Leaf evidence is only the fallback when the
;; owner has no retained slot certificate yet.
(content-slot-proof
(and (not owner-slot-proof)
(not fixed-flex-ancestor-proof)
(cl-loop
for entry in dirty-set
for proof = (and (memq :content
(plist-get entry :changed-keys))
(ebox-incremental--content-slot-stable-p
buffer entry candidate-state))
when proof return proof)))
(allocation-stable-p
(or owner-slot-proof fixed-flex-ancestor-proof
content-slot-proof))
(ancestor-slot-signature
(or owner-slot-proof fixed-flex-ancestor-proof content-slot-proof
(and snapshot
(list (list :node-id owner-id
:parent-slot-signature
(plist-get snapshot
:parent-slot-signature))))))
(allocated-width
(or
(when-let* ((capacity-proof
(or owner-slot-proof fixed-flex-ancestor-proof
content-slot-proof))
(slot-owner
(or (cadr capacity-proof)
(car capacity-proof)))
(slots
(plist-get
(plist-get slot-owner :parent-slot-signature)
:slots)))
(apply #'min
(mapcar
(lambda (slot)
(- (plist-get slot :end-pixel)
(plist-get slot :start-pixel)))
slots)))
(and snapshot
(plist-get
(plist-get snapshot :external-footprint-signature)
:max-line-pixel-width))))
(cascade-safe-p
(or (and (not (plist-get old-state :cascade-active-p))
(not (plist-get old-state :cascade-required-p))
(not (ebox-style-cascade-active-p)))
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state dirty-set))))
(let ((ebox-incremental--buffer-render-state-override
(cons buffer candidate-state))
(ebox-incremental--candidate-base-state
(cons buffer old-state)))
(when (and owner-id old-node new-node snapshot allocated-width
(not (equal owner-id root-id))
(memq (plist-get operation :op)
'(span-patch owner-rerender))
(cl-every
(lambda (entry)
(and (eq (plist-get entry :dirty-kind) 'geometry)
(not (plist-get entry :children))
(ebox--runtime-ancestor-id-p
buffer owner-id (plist-get entry :node-id))
(cl-every
(lambda (key)
(or (eq key :content)
(eq key :surface-properties)
(memq key ebox--paint-style-signature-keys)
(memq key
ebox--typography-style-signature-keys)
(memq key '(:font-family :font-size
:font-weight :font-style))))
(plist-get entry :changed-keys))))
dirty-set)
allocation-stable-p
;; Fixed-basis allocation is content-independent on the
;; flex main axis. The surface performs the one canonical
;; candidate render and exact old-slot footprint check, so
;; a second proof render here would only duplicate work.
(or owner-slot-proof fixed-flex-ancestor-proof
content-slot-proof)
(equal (gethash owner-id
(plist-get old-state :parent-table))
(gethash owner-id
(plist-get candidate-state :parent-table)))
(ebox-incremental--node-child-ids-equal-p
old-node new-node)
(equal (ebox-incremental--layout-slot-style-signature
old-node)
(ebox-incremental--layout-slot-style-signature
new-node))
(ebox-incremental--same-root-surface-identity-p
old-state candidate-state)
(null (plist-get old-state :scroll-region-ids))
(not (ebox--node-visible-overflow-p old-node))
(not (ebox--node-visible-overflow-p new-node))
ancestor-slot-signature
cascade-safe-p)
(let* ((owner-proof
(list :owner-id owner-id
:dirty-set (copy-sequence dirty-set)
:changed-keys (copy-sequence changed-keys)
:snapshot snapshot
:allocated-width allocated-width
:variable-content-p
(and allocated-width
(cl-some
(lambda (entry)
(memq :content
(plist-get entry :changed-keys)))
dirty-set))
:variable-content-max-width allocated-width
:role-owned-lines-p t
:ancestor-slot-signature ancestor-slot-signature
:layout-snapshot-detail-generation
(plist-get old-state
:layout-snapshot-detail-generation)
:static-cascade-p cascade-safe-p))
(owner-proofs (list owner-proof)))
(list :owner-ids (list owner-id)
:owner-proofs owner-proofs
:dirty-set (copy-sequence dirty-set)
:ancestor-slot-signature
(copy-sequence
(plist-get owner-proof :ancestor-slot-signature))
:layout-snapshot-detail-generation
(plist-get old-state :layout-snapshot-detail-generation)
:static-cascade-p cascade-safe-p)))))))
(defun ebox-incremental--range-span-proof
(buffer old-state prepared candidate-state owner-plan)
"Return a strict equal-cardinality Range replacement proof, or nil."
(when (and (= (length owner-plan) 1)
(= (length (plist-get prepared :dirty-set)) 1))
(let* ((operation (car owner-plan))
(dirty (car (plist-get prepared :dirty-set)))
(delta (plist-get dirty :range-delta))
(owner-id (plist-get operation :owner-id))
(old-node (gethash owner-id (plist-get old-state :node-table)))
(new-node (gethash owner-id
(plist-get candidate-state :node-table)))
(old-items (and delta (apply #'append
(plist-get delta :old-payloads))))
(new-items (and delta (apply #'append
(plist-get delta :new-payloads))))
(snapshot
(and old-node
(ebox-incremental--cached-layout-snapshot-details
buffer owner-id)))
(spans (and snapshot (plist-get snapshot :buffer-spans)))
(ancestor-slot-signature
(and owner-id
(ebox-incremental--ancestor-slot-signature
buffer old-state candidate-state owner-id)))
(cascade-safe-p
(or (and (not (plist-get old-state :cascade-active-p))
(not (plist-get old-state :cascade-required-p))
(not (ebox-style-cascade-active-p)))
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state (list dirty)))))
(when (and delta old-node new-node snapshot spans ancestor-slot-signature
(eq (plist-get operation :op) 'span-patch)
(eq (plist-get dirty :dirty-kind) 'structure)
(equal (plist-get dirty :changed-keys) '(:children))
old-items new-items
(= (length old-items) (length new-items))
(= (length spans) (length old-items))
(cl-every (lambda (item)
(eq (plist-get item :ebox-kind) 'box))
new-items)
(equal (gethash owner-id (plist-get old-state :parent-table))
(gethash owner-id
(plist-get candidate-state :parent-table)))
(equal (ebox-incremental--layout-slot-style-signature old-node)
(ebox-incremental--layout-slot-style-signature new-node))
(equal (ebox-tree-display-inner old-node) 'column)
(ebox-incremental--same-root-surface-identity-p
old-state candidate-state)
(null (plist-get old-state :scroll-region-ids))
(not (ebox-tree-node-visible-overflow-p old-node))
(not (ebox-tree-node-visible-overflow-p new-node))
cascade-safe-p)
(list :owner-id owner-id
:owner-ids (list owner-id)
:dirty-set (list dirty)
:snapshot snapshot
:old-spans spans
:allocated-width
(plist-get
(plist-get snapshot :external-footprint-signature)
:max-line-pixel-width)
:ancestor-slot-signature ancestor-slot-signature
:layout-snapshot-detail-generation
(plist-get old-state :layout-snapshot-detail-generation)
:static-cascade-p cascade-safe-p
:range-splice-p t)))))
(defun ebox-incremental--span-patch-projection-proof
(buffer old-state prepared candidate-state owner-plan
&optional path-copied-p)
"Return one atomic single- or multi-owner projection proof, or nil."
(if (= (length owner-plan) 1)
(when-let* ((proof
(or (ebox-incremental--single-span-patch-projection-proof
buffer old-state prepared candidate-state owner-plan
path-copied-p)
(ebox-incremental--grouped-owner-span-proof
buffer old-state prepared candidate-state owner-plan)
(ebox-incremental--range-span-proof
buffer old-state prepared candidate-state owner-plan))))
(unless (plist-get proof :owner-ids)
(setq proof
(plist-put proof :owner-ids
(list (plist-get proof :owner-id)))))
proof)
(let ((remaining (copy-sequence (plist-get prepared :dirty-set)))
proofs
valid)
(setq valid (and owner-plan t))
(dolist (operation owner-plan)
(when valid
(let* ((owned
(cl-remove-if-not
(lambda (dirty)
(ebox--patch-op-covers-node-p
buffer operation (plist-get dirty :node-id)
(if (eq (plist-get dirty :dirty-kind) 'paint)
'paint-patch
'span-patch)))
remaining))
(local-prepared (copy-sequence prepared))
proof)
(dolist (dirty owned)
(setq remaining (delq dirty remaining)))
(setq local-prepared
(plist-put local-prepared :dirty-set owned))
;; Each local proof owns only its assigned identity partition.
;; Other disjoint operations in the same atomic projection may
;; legitimately add or remove identities (notably a Range), so a
;; per-owner proof must not demand global node-set equality. The
;; aggregate branch still requires every dirty entry to be owned
;; and every resulting owner footprint to be disjoint.
(setq local-prepared
(plist-put local-prepared :partitioned-owner-proof-p t))
(setq proof
(and owned
(ebox-incremental--span-patch-projection-proof
buffer old-state local-prepared candidate-state
(list operation) path-copied-p)))
(if proof
(dolist (owner-proof
(or (plist-get proof :owner-proofs)
(list proof)))
(push owner-proof proofs))
(setq valid nil)))))
(setq valid (and valid (null remaining)))
(setq proofs (nreverse proofs))
(when (and valid
;; Variable content is admitted only when every owner has a
;; stable allocated slot proof. The surface will shape the
;; new line back into that retained slot before TP sees it;
;; raw string length is not the geometry proof.
(cl-every (lambda (proof)
(or (plist-get proof :variable-content-p)
(let* ((dirty (car (plist-get proof :dirty-set)))
(old-content
(plist-get
(plist-get dirty :old-signature)
:content))
(new-content
(plist-get
(plist-get dirty :new-signature)
:content)))
(and (stringp old-content)
(stringp new-content)
(= (length old-content)
(length new-content))))))
proofs)
(ebox-incremental--variable-span-proofs-safe-p
buffer proofs)
(ebox-incremental--owner-proofs-disjoint-p buffer proofs))
(let* ((owner-ids (mapcar (lambda (proof)
(plist-get proof :owner-id))
proofs))
(variable-p
(cl-some (lambda (proof)
(plist-get proof :variable-content-p))
proofs))
(capacities
(delq nil
(mapcar
(lambda (proof)
(plist-get proof :variable-content-max-width))
proofs)))
(common-max (and capacities (apply #'max capacities))))
(list :owner-ids owner-ids
:owner-proofs proofs
:variable-content-p
variable-p
:variable-content-max-width
(and variable-p common-max)
:dirty-set (copy-sequence (plist-get prepared :dirty-set))
:ancestor-slot-signature
(cl-mapcan
(lambda (proof)
(copy-sequence
(plist-get proof :ancestor-slot-signature)))
proofs)
:layout-snapshot-detail-generation
(plist-get old-state :layout-snapshot-detail-generation)
:static-cascade-p
(cl-every (lambda (proof)
(plist-get proof :static-cascade-p))
proofs)))))))
(defun ebox-incremental--owner-proof-list (proof)
"Return PROOF as one uniform list of leaf owner proofs."
(or (plist-get proof :owner-proofs)
(and (plist-get proof :owner-id) (list proof))))
(defun ebox-incremental--native-runtime-overrides (state)
"Return the retained native runtime facts owned by STATE."
(list :native-buffer-scroll-p
(plist-get state :native-buffer-scroll-p)
:native-sync-session
(plist-get state :native-sync-session)
:native-sync-pending
(plist-get state :native-sync-pending)
:native-sync-confirmed-p
(plist-get state :native-sync-confirmed-p)
:native-committed-rendered
(plist-get state :native-committed-rendered)
:native-committed-fragment-template
(plist-get state :native-committed-fragment-template)
:native-committed-owned-ranges
(plist-get state :native-committed-owned-ranges)
:native-fragment-owner-cache
(plist-get state :native-fragment-owner-cache)))
(defun ebox-incremental--surface-state-overrides
(old-state prepared candidate-state projection-kind
&optional span-proof source-path-copied-p formatting-proof)
"Return surface overrides for PREPARED CANDIDATE-STATE."
(let* ((scroll-fast-p
(and (eq projection-kind 'scroll-patch)
(plist-get prepared :scroll-patch-fast-p)))
(certificate-proof
(cond
((memq projection-kind '(span-patch owner-scoped)) span-proof)
((and (eq projection-kind 'mixed-owner-reflow)
(eq (plist-get (plist-get prepared :mixed-owner-proof)
:geometry-kind)
'span-patch))
(plist-get (plist-get prepared :mixed-owner-proof)
:geometry-proof))))
(certificate-proofs
(and certificate-proof
(ebox-incremental--owner-proof-list certificate-proof)))
(overrides
(append
(list :viewport-width (plist-get old-state :viewport-width)
:viewport-height (plist-get old-state :viewport-height)
:runtime-revision
(plist-get candidate-state :runtime-revision)
:display-signature (plist-get prepared :display-signature)
:source-index (plist-get candidate-state :source-index)
:scroll-state-table (plist-get prepared :scroll-state-table))
(ebox-incremental--native-runtime-overrides candidate-state)
(list :source-path-copied-p source-path-copied-p
:runtime-index-prepared-p
(or (plist-get prepared :runtime-index-complete-p)
(memq projection-kind
'(span-patch owner-scoped scroll-patch
formatting-context-reflow mixed-owner-reflow
viewport-reflow native-frame)))
:logical-candidate-p
(plist-get prepared :logical-candidate-p)
:computed-participation-validated-p
(plist-get prepared :computed-participation-validated-p)
:detached-identity-history
(plist-get prepared :detached-identity-history)))))
(setq overrides
(plist-put overrides :native-touched-node-ids
(or (plist-get candidate-state
:native-touched-node-ids)
(plist-get prepared :touched-node-ids))))
(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
(plist-get candidate-state :native-topology-stable-p)))
(if (and (not (memq projection-kind
'(paint span-patch owner-scoped scroll-patch
formatting-context-reflow mixed-owner-reflow
native-frame)))
(not (plist-get prepared :runtime-index-complete-p)))
overrides
(append
overrides
(list :layout-snapshots
(cond
((eq projection-kind 'owner-scoped)
(copy-hash-table (plist-get old-state :layout-snapshots)))
((and scroll-fast-p
(eq projection-kind 'scroll-patch))
;; The candidate already owns a hash-spine copy. Its detail
;; generation was advanced during preparation, so old buffer
;; spans become lazily stale without copying 512 snapshot
;; plists on every wheel event.
(plist-get candidate-state :layout-snapshots))
(t (plist-get candidate-state :layout-snapshots)))
:layout-snapshots-complete-p
(and (not scroll-fast-p)
(plist-get candidate-state :layout-snapshots-complete-p))
:layout-snapshot-detail-generation
(plist-get candidate-state :layout-snapshot-detail-generation)
:render-cache (plist-get candidate-state :render-cache)
:render-signature-cache
(plist-get candidate-state :render-signature-cache)
:flex-content-min-widths
(if (or (memq projection-kind
'(span-patch owner-scoped))
scroll-fast-p)
(plist-get candidate-state :flex-content-min-widths)
(plist-get old-state :flex-content-min-widths))
:viewport-height-dependent-subtree-cache
(plist-get candidate-state
:viewport-height-dependent-subtree-cache)
:viewport-dependent-node-ids-ready
(plist-get candidate-state :viewport-dependent-node-ids-ready)
:viewport-dependent-node-ids
(plist-get candidate-state :viewport-dependent-node-ids)
:viewport-dependent-node-id-axes
(plist-get candidate-state :viewport-dependent-node-id-axes)
:scroll-region-ids (plist-get candidate-state :scroll-region-ids)
:node-table (plist-get candidate-state :node-table)
:parent-table (plist-get candidate-state :parent-table)
:region-id-set (plist-get candidate-state :region-id-set)
:region-node-table (plist-get candidate-state :region-node-table)
:region-box-count-table
(plist-get candidate-state :region-box-count-table)
:region-box-table (plist-get candidate-state :region-box-table)
:range-ref-table (plist-get candidate-state :range-ref-table)
:source-index (plist-get candidate-state :source-index)
:selector-id-table (plist-get candidate-state :selector-id-table)
:selector-class-table
(plist-get candidate-state :selector-class-table)
:selector-type-table
(plist-get candidate-state :selector-type-table)
:runtime-type-count-table
(plist-get candidate-state :runtime-type-count-table)
:selector-index-stale-p
(plist-get candidate-state :selector-index-stale-p)
:native-node-postorder
(plist-get candidate-state :native-node-postorder)
:surface-node-object-table
(and (memq projection-kind
'(span-patch owner-scoped scroll-patch
formatting-context-reflow mixed-owner-reflow))
(plist-get old-state :surface-node-object-table))
:region-surface-object-table
(and scroll-fast-p
(plist-get old-state :region-surface-object-table))
:surface-object-region-table
(and scroll-fast-p
(plist-get old-state :surface-object-region-table))
:logical-id-region-table
(and scroll-fast-p
(plist-get old-state :logical-id-region-table))
:paint-dirty-set
(and (eq projection-kind 'paint)
(plist-get prepared :dirty-set))
:span-patch-owner-id
(or (and span-proof (plist-get span-proof :owner-id))
(and formatting-proof
(plist-get formatting-proof :owner-id))
(and (plist-get prepared :mixed-owner-proof)
(plist-get
(plist-get (plist-get prepared :mixed-owner-proof)
:geometry-proof)
:owner-id)))
:span-patch-dirty-set
(or (and span-proof (plist-get span-proof :dirty-set))
(and (eq projection-kind 'mixed-owner-reflow)
(plist-get (plist-get prepared :mixed-owner-proof)
:dirty-set)))
:owner-scoped-allocated-width
(and (eq projection-kind 'owner-scoped)
(plist-get span-proof :allocated-width))
:owner-scoped-proofs
(or (and (memq projection-kind '(span-patch owner-scoped))
(ebox-incremental--owner-proof-list span-proof))
(and (eq projection-kind 'mixed-owner-reflow)
(eq (plist-get (plist-get prepared
:mixed-owner-proof)
:geometry-kind)
'span-patch)
(ebox-incremental--owner-proof-list
(plist-get (plist-get prepared :mixed-owner-proof)
:geometry-proof))))
:formatting-context-reflow-proof
(and (or (eq projection-kind 'formatting-context-reflow)
(and (eq projection-kind 'mixed-owner-reflow)
(eq (plist-get (plist-get prepared
:mixed-owner-proof)
:geometry-kind)
'formatting-context-reflow)))
(or formatting-proof
(plist-get (plist-get prepared :mixed-owner-proof)
:geometry-proof)))
:mixed-owner-proof
(and (eq projection-kind 'mixed-owner-reflow)
(plist-get prepared :mixed-owner-proof))
:allocation-certificate-proofs certificate-proofs
:scroll-patch-region-id nil
:scroll-state-transaction nil)))))
(defun ebox-incremental--same-root-surface-identity-p
(old-state candidate-state)
"Return non-nil when OLD-STATE and CANDIDATE-STATE retain one TP root."
(let ((old-root (plist-get old-state :root-node))
(new-root (plist-get candidate-state :root-node)))
(and (eq (plist-get old-root :ebox-type)
(plist-get new-root :ebox-type))
(equal
(ebox-tree-node-key (plist-get old-state :source-index) old-root)
(ebox-tree-node-key
(plist-get candidate-state :source-index) new-root)))))
(defun ebox-incremental--surface-commit-input
(buffer old-state prepared)
"Return TP surface input for BUFFER from OLD-STATE and PREPARED runtime."
(let* ((ebox-incremental--allocated-slot-proof-cache
(make-hash-table :test 'equal))
(ebox--flex-slot-safety-cache (make-hash-table :test 'equal))
(ebox--runtime-ancestor-cache (make-hash-table :test 'equal))
(candidate-state
(ebox-incremental--candidate-state
old-state (plist-get prepared :root)
(plist-get prepared :index) prepared))
(ebox--render-source-index
(plist-get candidate-state :source-index))
(ebox--render-source-generations
(list (plist-get candidate-state :source-index)
(plist-get old-state :source-index)))
(ebox--render-source-states (list candidate-state old-state))
(_selector-index-stale
(when (and (plist-get candidate-state :source-index)
(ebox-source--index-derived-stale-p
(plist-get candidate-state :source-index)))
(plist-put candidate-state :selector-index-stale-p t)))
(structural-dirty-p
(cl-some (lambda (entry)
(eq (plist-get entry :dirty-kind) 'structure))
(plist-get prepared :dirty-set)))
(range-structural-p
(and structural-dirty-p
(cl-every
(lambda (entry)
(or (not (eq (plist-get entry :dirty-kind) 'structure))
(plist-get entry :range-delta)))
(plist-get prepared :dirty-set))))
(native-program-p
(and (or (eq (plist-get old-state :projection-kind) 'native-frame)
(>= (length (plist-get prepared :dirty-set)) 3)
structural-dirty-p
(> (or (plist-get prepared :range-replacement-count) 0) 0))
(zerop (ebox-tree-author-style-count
(plist-get old-state :root-node)))
(zerop (ebox-tree-author-style-count
(plist-get candidate-state :root-node)))
(require 'ebox-native-commit nil t)
(ebox-native-commit-plan-eligible-p
old-state candidate-state prepared)))
(_normalize-local-dirty
(when (and (not native-program-p)
(plist-get prepared :styles-prepared-p))
(setq prepared
(plist-put
prepared :dirty-set
(ebox-incremental--computed-local-dirty-set
old-state (plist-get prepared :index)
(plist-get prepared :dirty-set))))
(setq structural-dirty-p
(cl-some
(lambda (entry)
(eq (plist-get entry :dirty-kind) 'structure))
(plist-get prepared :dirty-set)))
(setq range-structural-p
(and structural-dirty-p
(cl-every
(lambda (entry)
(or (not (eq (plist-get entry :dirty-kind)
'structure))
(plist-get entry :range-delta)))
(plist-get prepared :dirty-set))))))
(formatting-proof
(and (not native-program-p)
(not (cl-some
(lambda (entry)
(eq (plist-get entry :dirty-kind) 'paint))
(plist-get prepared :dirty-set)))
(ebox-incremental--formatting-context-reflow-proof
buffer old-state candidate-state prepared)))
(mixed-shape-p
(and (not native-program-p)
(or (not structural-dirty-p) range-structural-p)
(cl-some
(lambda (entry)
(eq (plist-get entry :dirty-kind) 'paint))
(plist-get prepared :dirty-set))
(>= (cl-count-if
(lambda (entry)
(or (eq (plist-get entry :dirty-kind) 'geometry)
(and (eq (plist-get entry :dirty-kind) 'structure)
(plist-get entry :range-delta))))
(plist-get prepared :dirty-set))
1)))
(mixed-proof
(and mixed-shape-p
(ebox-incremental--mixed-owner-proof
buffer old-state candidate-state prepared nil)))
(mixed-owner-plan
(and mixed-proof
;; The proof already owns the exact geometry/paint union.
;; These lightweight ops feed the generic report and scope
;; builders; they never re-run layout ownership planning.
(append
(mapcar
(lambda (owner-id)
(ebox--patch-op
(if (eq (plist-get mixed-proof :geometry-kind)
'formatting-context-reflow)
'formatting-context-reflow
'span-patch)
owner-id))
(plist-get mixed-proof :geometry-owner-ids))
(mapcar (lambda (owner-id)
(ebox--patch-op 'paint-patch owner-id))
(plist-get mixed-proof :paint-owner-ids)))))
(owner-plan
(cond
((/= (ebox-tree-author-style-count
(plist-get old-state :root-node))
(ebox-tree-author-style-count
(plist-get candidate-state :root-node)))
(ebox-incremental--root-owner-plan
buffer (plist-get prepared :dirty-set)))
(native-program-p
(ebox-incremental--native-frame-plan
buffer (plist-get prepared :dirty-set)))
(mixed-owner-plan mixed-owner-plan)
(formatting-proof
(list
(ebox--patch-op
'formatting-context-reflow
(plist-get formatting-proof :owner-id)
:dirty (plist-get formatting-proof :dirty-set)
:proof formatting-proof)))
(t
(let ((ebox-incremental--candidate-state-for-slot-proof
candidate-state))
(if (ebox-incremental--cascade-root-owner-plan-p
old-state candidate-state
(plist-get prepared :dirty-set))
(ebox-incremental--root-owner-plan
buffer (plist-get prepared :dirty-set))
(ebox-incremental--layout-owner-plan
buffer old-state candidate-state
(plist-get prepared :dirty-set)))))))
(same-root-identity-p
(ebox-incremental--same-root-surface-identity-p
old-state candidate-state))
(scope-node-ids
(and same-root-identity-p
(delete-dups
(or (mapcar (lambda (op) (plist-get op :owner-id)) owner-plan)
(mapcar (lambda (entry) (plist-get entry :node-id))
(plist-get prepared :dirty-set))))))
(report
(ebox-incremental--layout-owner-report
prepared owner-plan candidate-state))
(paint-p
(and (not native-program-p)
(ebox-incremental--paint-projection-p
old-state owner-plan (plist-get prepared :dirty-set))))
(span-proof
(unless (or native-program-p structural-dirty-p paint-p mixed-proof)
(ebox-incremental--span-patch-projection-proof
buffer old-state prepared candidate-state owner-plan)))
(projection-kind
(cond
(native-program-p 'native-frame)
(mixed-proof 'mixed-owner-reflow)
(formatting-proof 'formatting-context-reflow)
(paint-p 'paint)
((and span-proof
(plist-get span-proof :static-cascade-p)
(not (plist-get span-proof :variable-content-p)))
'owner-scoped)
(span-proof 'span-patch))))
(when projection-kind
(setq report (plist-put report :projection-kind projection-kind)))
(when mixed-proof
(setq prepared (plist-put prepared :mixed-owner-proof mixed-proof))
(setq report
(ebox-incremental--plist-overlay
report
(list :strategy 'mixed-owner-reflow
:owner-ids (plist-get mixed-proof :owner-ids)
:patch-count (length (plist-get mixed-proof :owner-ids))
:publication-scope 'mixed-owner-union
:render-scope 'mixed-owner-union))))
(when span-proof
(setq report
(plist-put report :ancestor-slot-count
(length (plist-get span-proof
:ancestor-slot-signature))))
(setq report
(plist-put report :ancestor-slot-generation
(plist-get span-proof
:layout-snapshot-detail-generation))))
(unless same-root-identity-p
(setq report (plist-put report :strategy 'root-rerender))
(setq report (plist-put report :publication-scope 'root))
(setq report (plist-put report :root-rerender t)))
(when (> (or (plist-get prepared :range-replacement-count) 0) 0)
(let* ((range-metrics (copy-tree (plist-get prepared :range-metrics)))
(applied (cl-loop for entry in range-metrics
sum (plist-get entry :ref-index-visits)))
(requested (plist-get prepared :range-replacement-count)))
(setq report
(plist-put report :range-metrics
(list :parents range-metrics
:replacement-count requested
:applied-count applied
:coalesced-count (- requested applied))))))
(list :root (plist-get prepared :root)
:scope-node-ids scope-node-ids
:report-base report
;; A child Range replacement can legitimately change the separator
;; owned by an adjacent sibling when its item count changes. Keep
;; the fast scoped proof for stable topology, but let the surface
;; publisher use its exact root fallback when that proof cannot
;; establish the outside text invariant.
:on-mismatch
(and (or (> (or (plist-get prepared :range-replacement-count) 0)
0)
(not
(let ((old-ranges (plist-get old-state :range-ref-table))
(new-ranges
(plist-get candidate-state :range-ref-table)))
(or (and (null old-ranges) (null new-ranges))
(ebox-incremental--hash-key-set-equal-p
old-ranges new-ranges)))))
'root)
:projection-kind projection-kind
:preserve-identities-p t
:state-overrides
(ebox-incremental--surface-state-overrides
old-state prepared candidate-state projection-kind span-proof
nil formatting-proof))))
(defun ebox-incremental--plist-overlay (base overrides)
"Return BASE with every property in OVERRIDES replaced."
(setq base (copy-sequence base))
(while overrides
(setq base (plist-put base (pop overrides) (pop overrides))))
base)
(defun ebox-incremental--prepare-scoped-candidate
(buffer old-state next-root isolated-candidate-p
&optional skip-span-proof-p scroll-transaction-p)
"Prepare NEXT-ROOT and its scoped owner plan for BUFFER.
Return the intermediate candidate state, owner plan, projection proof, and
whether NEXT-ROOT shares untouched published nodes."
(let* ((ebox-incremental--allocated-slot-proof-cache
(make-hash-table :test 'equal))
(source-base-index
(or ebox-incremental--source-base-index
(and isolated-candidate-p
(plist-get old-state :source-index))))
(path-copied-p
(and isolated-candidate-p
(ebox-incremental--candidate-shares-published-node-p
old-state next-root)))
(incoming-source-indexes
(when ebox-incremental--source-base-index
(let ((table (make-hash-table :test #'eq)))
(dolist (handle
(ebox-source--index-local-handles
ebox-incremental--source-base-index))
(puthash handle ebox-incremental--source-base-index table))
table)))
(local-preparation
(and path-copied-p
(let ((ebox-incremental--candidate-incoming-source-indexes
incoming-source-indexes))
(ebox-incremental--candidate-local-index-delta
old-state next-root))))
(source-index
(and source-base-index
(not local-preparation)
(if (ebox-source--index-node-subjects source-base-index)
;; Region/property candidates preserve validated topology;
;; their source rebind already patches the one exact subject
;; handle, so rebuilding every tree-derived subject would
;; turn an O(changed) update back into O(all).
source-base-index
(ebox-tree-source-index
next-root nil nil source-base-index))))
(prepared
(if isolated-candidate-p
(ebox-incremental--prepare-declarative-runtime
buffer old-state next-root nil local-preparation
skip-span-proof-p source-index)
(ebox-incremental--prepare-declarative-root
buffer old-state next-root)))
(candidate-state
(ebox-incremental--candidate-state
old-state (plist-get prepared :root)
(plist-get prepared :index) prepared))
(ebox--render-source-index
(plist-get candidate-state :source-index))
(ebox--render-source-generations
(list (plist-get candidate-state :source-index)
(plist-get old-state :source-index)))
(ebox--render-source-states (list candidate-state old-state))
(_selector-index-stale
(when (and (plist-get candidate-state :source-index)
(ebox-source--index-derived-stale-p
(plist-get candidate-state :source-index)))
(plist-put candidate-state :selector-index-stale-p t)))
(formatting-proof
(and (not scroll-transaction-p)
(not (cl-some
(lambda (entry)
(eq (plist-get entry :dirty-kind) 'paint))
(plist-get prepared :dirty-set)))
(ebox-incremental--formatting-context-reflow-proof
buffer old-state candidate-state prepared)))
(owner-plan
(if formatting-proof
(list
(ebox--patch-op
'formatting-context-reflow
(plist-get formatting-proof :owner-id)
:dirty (plist-get formatting-proof :dirty-set)
:proof formatting-proof))
(unless scroll-transaction-p
(let ((ebox-incremental--candidate-state-for-slot-proof
candidate-state))
(ebox-incremental--layout-owner-plan
buffer old-state candidate-state
(plist-get prepared :dirty-set))))))
(mixed-proof
(and (not scroll-transaction-p)
(ebox-incremental--mixed-owner-proof
buffer old-state candidate-state prepared owner-plan)))
(scope-node-ids
(delete-dups
(or (mapcar (lambda (op) (plist-get op :owner-id)) owner-plan)
(mapcar (lambda (entry) (plist-get entry :node-id))
(plist-get prepared :dirty-set)))))
(paint-p
(ebox-incremental--paint-projection-p
old-state owner-plan (plist-get prepared :dirty-set)))
(span-proof
(unless (or paint-p mixed-proof skip-span-proof-p)
(ebox-incremental--span-patch-projection-proof
buffer old-state prepared candidate-state owner-plan
path-copied-p)))
(projection-kind
(cond
(mixed-proof 'mixed-owner-reflow)
(formatting-proof 'formatting-context-reflow)
(paint-p 'paint)
((and span-proof
(plist-get span-proof :static-cascade-p)
(not (plist-get span-proof :variable-content-p)))
'owner-scoped)
(span-proof 'span-patch))))
(setq prepared (plist-put prepared :scroll-patch-fast-p
skip-span-proof-p))
(when mixed-proof
(setq prepared (plist-put prepared :mixed-owner-proof mixed-proof)))
(list :prepared prepared
:candidate-state candidate-state
:owner-plan owner-plan
:scope-node-ids scope-node-ids
:span-proof span-proof
:mixed-proof mixed-proof
:formatting-proof formatting-proof
:projection-kind projection-kind
:path-copied-p path-copied-p)))
(defun ebox-incremental-prepare-scoped-commit
(buffer next-root report-overrides &optional isolated-candidate-p)
"Prepare NEXT-ROOT for object-scoped TP publication in BUFFER.
REPORT-OVERRIDES replaces Ebox semantic report fields. The returned plist
contains the isolated candidate root, Ebox node identities, report, and
runtime overrides; the surface layer resolves identities to opaque TP objects.
When ISOLATED-CANDIDATE-P is non-nil, NEXT-ROOT is an internally copied
runtime candidate whose identities already match the published root."
(let ((old-state (ebox--buffer-render-state buffer))
(scroll-transaction-p
(plist-get report-overrides :scroll-state-transaction))
(scroll-patch-fast-p
(plist-get report-overrides :scroll-patch-fast-p)))
(unless old-state
(error "Ebox buffer has no rendered runtime: %S" buffer))
(let* ((candidate
(ebox-incremental--prepare-scoped-candidate
buffer old-state next-root isolated-candidate-p
scroll-patch-fast-p scroll-transaction-p)))
;; A path-copied candidate is safe to render in place only after the
;; strict span proof succeeds. Widened projection would mutate shared
;; layout nodes, so promote it to a private copy before the surface sees
;; it. The ordinary full-copy path remains the correctness fallback.
(when (and (plist-get candidate :path-copied-p)
(not scroll-patch-fast-p)
(not (memq (plist-get candidate :projection-kind)
'(span-patch owner-scoped scroll-patch
mixed-owner-reflow))))
(setq candidate
(ebox-incremental--prepare-scoped-candidate
buffer old-state
(ebox-tree-copy-node-structure
(plist-get (plist-get candidate :prepared) :root))
isolated-candidate-p scroll-patch-fast-p
scroll-transaction-p)))
(let* ((prepared (plist-get candidate :prepared))
(candidate-state (plist-get candidate :candidate-state))
(owner-plan
(or (plist-get candidate :owner-plan)
(and scroll-transaction-p
(list
(ebox--patch-op
'span-patch
(plist-get report-overrides :owner-id)
:dirty (plist-get prepared :dirty-set)
:proof-skipped-p t)))))
(scope-node-ids
(if scroll-transaction-p
(list (plist-get report-overrides :owner-id))
(plist-get candidate :scope-node-ids)))
(span-proof (plist-get candidate :span-proof))
(formatting-proof (plist-get candidate :formatting-proof))
(projection-kind (plist-get candidate :projection-kind))
(report
(ebox-incremental--plist-overlay
(ebox-incremental--layout-owner-report
prepared owner-plan candidate-state)
report-overrides)))
(when scroll-transaction-p
;; Scroll changes already carry a bounded rendered window in the
;; staged scroll state. Publish that window through the retained
;; object scope instead of re-running the full root layout.
(setq projection-kind 'scroll-patch)
(setq report (plist-put report :projection-kind projection-kind)))
(when projection-kind
(setq report (plist-put report :projection-kind projection-kind)))
(when span-proof
(setq report
(plist-put report :ancestor-slot-count
(length (plist-get span-proof
:ancestor-slot-signature))))
(setq report
(plist-put report :ancestor-slot-generation
(plist-get span-proof
:layout-snapshot-detail-generation))))
(let ((state-overrides
(ebox-incremental--surface-state-overrides
old-state prepared candidate-state projection-kind span-proof
(plist-get candidate :path-copied-p) formatting-proof)))
(when scroll-transaction-p
(setq state-overrides
(plist-put state-overrides :scroll-state-transaction t))
(setq state-overrides
(plist-put state-overrides :scroll-patch-region-id
(plist-get report-overrides :region-id)))
(setq state-overrides
(plist-put state-overrides :span-patch-owner-id
(or (plist-get state-overrides :span-patch-owner-id)
(plist-get report-overrides :owner-id))))
(setq state-overrides
(plist-put state-overrides :scroll-fast-window-p
scroll-patch-fast-p)))
(list :root (plist-get prepared :root)
:scope-node-ids scope-node-ids
:report-base report
:projection-kind projection-kind
:preserve-identities-p t
:state-overrides state-overrides))))))
(defun ebox-incremental--viewport-state-overrides
(state &optional retain-native-p)
"Return surface overrides from viewport candidate STATE.
When RETAIN-NATIVE-P is non-nil, preserve its committed native frame bases."
(append
(list :viewport-width (plist-get state :viewport-width)
:viewport-height (plist-get state :viewport-height)
;; Candidate runtime revisions are the TP base revision for the
;; upcoming viewport transaction; the published TP revision is
;; advanced by the transaction and recorded after publication.
:runtime-revision (plist-get state :runtime-revision)
:display-signature (plist-get state :display-signature)
:render-cache (plist-get state :render-cache)
:layout-fragments (plist-get state :layout-fragments)
:layout-fragments-reuse-p
(plist-get state :layout-fragments-reuse-p)
:render-signature-cache (plist-get state :render-signature-cache)
:flex-content-min-widths (plist-get state :flex-content-min-widths)
:viewport-height-dependent-subtree-cache
(plist-get state :viewport-height-dependent-subtree-cache)
:viewport-dependent-node-ids-ready
(plist-get state :viewport-dependent-node-ids-ready)
:viewport-dependent-node-ids
(plist-get state :viewport-dependent-node-ids)
:viewport-dependent-node-id-axes
(plist-get state :viewport-dependent-node-id-axes)
;; A viewport commit never changes the declarative node topology.
;; Preserve the prepared indexes so the surface layer does not walk
;; the complete runtime tree again after rendering the new geometry.
:node-table (plist-get state :node-table)
:parent-table (plist-get state :parent-table)
:region-id-set (plist-get state :region-id-set)
:region-node-table (plist-get state :region-node-table)
:region-box-count-table (plist-get state :region-box-count-table)
:source-index (plist-get state :source-index)
:range-ref-table (plist-get state :range-ref-table)
:selector-id-table (plist-get state :selector-id-table)
:selector-class-table (plist-get state :selector-class-table)
:selector-type-table (plist-get state :selector-type-table)
:runtime-type-count-table (plist-get state :runtime-type-count-table)
:native-node-postorder (plist-get state :native-node-postorder)
:runtime-index-prepared-p t
:scroll-state-table (plist-get state :scroll-state-table)
:detached-identity-history
(plist-get state :detached-identity-history))
(and retain-native-p
(ebox-incremental--native-runtime-overrides state))))
(defun ebox-incremental--viewport-root-proof-p
(old-state candidate-state dirty-set owner-plan style-context-stable-p)
"Return non-nil when a retained viewport root proof can begin."
(let* ((old-root (plist-get old-state :root-node))
(candidate-root (plist-get candidate-state :root-node))
(root-id (and old-root (plist-get old-root :node-id)))
(operation (and (= (length owner-plan) 1) (car owner-plan)))
(objects (plist-get old-state :surface-node-object-table)))
(and old-root candidate-root
(eq old-root candidate-root)
(eq (plist-get operation :op) 'owner-rerender)
(equal (plist-get operation :owner-id) root-id)
(cl-every (lambda (dirty)
(and (eq (plist-get dirty :dirty-kind) 'geometry)
(not (plist-get dirty :children))))
dirty-set)
(or (and (not (plist-get old-state :cascade-active-p))
(not (ebox-style-cascade-active-p)))
style-context-stable-p)
(zerop (ebox-tree-author-style-count old-root))
(zerop (ebox-tree-author-style-count candidate-root))
(not (ebox-tree-node-visible-overflow-p old-root))
(hash-table-p objects)
(> (hash-table-count objects) 0)
(ebox-incremental--hash-key-set-equal-p
(plist-get old-state :node-table)
(plist-get candidate-state :node-table))
(ebox-incremental--hash-key-set-equal-p
(plist-get old-state :region-id-set)
(plist-get candidate-state :region-id-set))
(ebox-incremental--hash-key-set-equal-p
(plist-get old-state :parent-table)
(plist-get candidate-state :parent-table)))))
(defun ebox-incremental--viewport-reflow-safe-p
(old-state candidate-state dirty-set owner-plan style-context-stable-p)
"Return non-nil when a viewport reflow can reuse the retained node tree.
This strict projection retains only viewport-independent scroll producers."
(and (ebox-incremental--viewport-root-proof-p
old-state candidate-state dirty-set owner-plan
style-context-stable-p)
(or (ebox-incremental--stable-scroll-state-p old-state)
(ebox-incremental--viewport-root-scroll-reflow-proof-p
old-state candidate-state style-context-stable-p))))
(defun ebox-incremental--viewport-root-scroll-reflow-proof-p
(old-state candidate-state style-context-stable-p)
"Return non-nil for one resizable root scroll owner.
The root itself is the only scroll producer, so its viewport-dependent
content is exactly the formatting context being reflowed. This proof does
not authorize nested scroll owners, visible overflow, cascade changes, or
topology changes; those continue to use the conservative full projection."
(let* ((old-root (plist-get old-state :root-node))
(candidate-root (plist-get candidate-state :root-node))
(root-id (and old-root (plist-get old-root :node-id)))
(scroll-ids (plist-get old-state :scroll-region-ids))
(region-node-table (plist-get old-state :region-node-table))
(scroll-table (plist-get old-state :scroll-state-table))
(region-id (and (= (length scroll-ids) 1)
(car scroll-ids)))
(region-node-id (and region-id region-node-table
(gethash region-id region-node-table)))
(candidate-scroll-table
(plist-get candidate-state :scroll-state-table)))
(and old-root candidate-root
(eq old-root candidate-root)
root-id region-id
(equal region-node-id root-id)
(hash-table-p scroll-table)
(gethash region-id scroll-table)
(hash-table-p candidate-scroll-table)
(ebox-incremental--hash-key-set-equal-p
scroll-table candidate-scroll-table)
(not (ebox-tree-node-visible-overflow-p old-root))
(or (and (not (plist-get old-state :cascade-active-p))
(not (ebox-style-cascade-active-p)))
style-context-stable-p)
(zerop (ebox-tree-author-style-count old-root))
(zerop (ebox-tree-author-style-count candidate-root))
(ebox-incremental--hash-key-set-equal-p
(plist-get old-state :node-table)
(plist-get candidate-state :node-table))
(ebox-incremental--hash-key-set-equal-p
(plist-get old-state :region-id-set)
(plist-get candidate-state :region-id-set))
(ebox-incremental--hash-key-set-equal-p
(plist-get old-state :parent-table)
(plist-get candidate-state :parent-table)))))
(defun ebox-incremental--stable-scroll-state-p (state)
"Return non-nil when STATE's scroll producers are safe to retain.
The proof is local to each scroll owner: its measured subtree must be
independent of both viewport axes, must not spill visible overflow, and must
have a published scroll state. A changing ancestor position is harmless;
the producer's content geometry is the retained boundary."
(let ((scroll-ids (plist-get state :scroll-region-ids))
(region-node-table (plist-get state :region-node-table))
(node-table (plist-get state :node-table))
(scroll-state-table (plist-get state :scroll-state-table)))
(or (null scroll-ids)
(and (hash-table-p region-node-table)
(hash-table-p node-table)
(hash-table-p scroll-state-table)
(cl-every
(lambda (region-id)
(let* ((node-id (gethash region-id region-node-table))
(node (and node-id (gethash node-id node-table))))
(and node
(gethash region-id scroll-state-table)
(not (ebox--viewport-dependent-subtree-p node))
(not (ebox--viewport-height-dependent-subtree-p node))
(not (ebox-tree-node-visible-overflow-p node)))))
scroll-ids)))))
(defun ebox-incremental--viewport-scroll-partition (state)
"Partition STATE scroll owners into stable and viewport-affected sets.
A producer is stable only when its own subtree is independent of both
viewport axes and has no visible overflow; an affected producer is rendered
fresh by the mixed projection."
(let ((region-node-table (plist-get state :region-node-table))
(node-table (plist-get state :node-table))
(scroll-state-table (plist-get state :scroll-state-table))
(stable nil)
(affected nil))
(when (and (hash-table-p region-node-table)
(hash-table-p node-table)
(hash-table-p scroll-state-table))
(let ((ebox--viewport-dependent-node-ids-cache
(make-hash-table :test #'eq))
(ebox--viewport-dependent-subtree-cache
(make-hash-table :test #'eq))
(ebox--viewport-height-dependent-subtree-cache
(make-hash-table :test #'eq)))
(dolist (region-id (plist-get state :scroll-region-ids))
(let* ((node-id (gethash region-id region-node-table))
(node (and node-id (gethash node-id node-table)))
(scroll-state (gethash region-id scroll-state-table))
(stable-p
(and node scroll-state
(not (ebox--viewport-dependent-subtree-p node))
(not (ebox--viewport-height-dependent-subtree-p node))
(not (ebox-tree-node-visible-overflow-p node)))))
(if stable-p
(push region-id stable)
(push region-id affected)))))
(setq stable (nreverse stable)
affected (nreverse affected))
(list :stable stable
:affected affected
:all (append stable affected)))))
(defun ebox-incremental--viewport-mixed-scroll-proof-p
(old-state candidate-state dirty-set owner-plan partition
style-context-stable-p)
"Return PARTITION when a mixed viewport projection is proven safe."
(and (ebox-incremental--viewport-root-proof-p
old-state candidate-state dirty-set owner-plan
style-context-stable-p)
(plist-get partition :stable)
(plist-get partition :affected)
partition))
(defun ebox-incremental--viewport-reflow-state-overrides
(old-state candidate-state &optional scroll-partition)
"Return candidate STATE overrides for a retained viewport reflow."
(append
(ebox-incremental--viewport-state-overrides candidate-state)
(list
:preserve-scroll-producer-region-ids
(plist-get scroll-partition :stable)
:scroll-region-ids (plist-get candidate-state :scroll-region-ids)
:node-table (plist-get candidate-state :node-table)
:parent-table (plist-get candidate-state :parent-table)
:region-id-set (plist-get candidate-state :region-id-set)
:region-node-table (plist-get candidate-state :region-node-table)
:region-box-count-table (plist-get candidate-state :region-box-count-table)
:region-box-table (plist-get candidate-state :region-box-table)
:range-ref-table (plist-get candidate-state :range-ref-table)
:selector-id-table (plist-get candidate-state :selector-id-table)
:selector-class-table (plist-get candidate-state :selector-class-table)
:selector-type-table (plist-get candidate-state :selector-type-table)
:runtime-type-count-table
(plist-get candidate-state :runtime-type-count-table)
:selector-index-stale-p
(plist-get candidate-state :selector-index-stale-p)
:native-node-postorder
(plist-get candidate-state :native-node-postorder)
:runtime-index-prepared-p t
:surface-node-object-table
(plist-get old-state :surface-node-object-table))))
(defun ebox-incremental-prepare-viewport-commit
(buffer viewport-width viewport-height axes display-signature
style-context-stable-p)
"Prepare BUFFER for VIEWPORT-WIDTH, VIEWPORT-HEIGHT, AXES, and DISPLAY-SIGNATURE."
(let* ((old-state (ebox--buffer-render-state buffer))
(ebox--render-source-index (plist-get old-state :source-index))
(change (ebox--viewport-constraint-change buffer axes))
(display-changed
(not (equal display-signature
(plist-get old-state :display-signature))))
(dirty-set
(if display-changed
(let ((root (plist-get old-state :root-node)))
(list
(ebox--dirty-entry
(plist-get root :node-id) 'geometry
:changed-keys '(:display-signature)
:old-region-ids (ebox--node-all-region-ids root)
:new-region-ids (ebox--node-all-region-ids root))))
(plist-get change :dirty-set)))
(target-height
(or viewport-height (plist-get old-state :viewport-height))))
(setq change (plist-put change :dirty-set dirty-set))
(if (null dirty-set)
(list :no-op t
:report-base
(append
(ebox--update-report
nil 'no-op :dirty-count 0 :patch-count 0 :patch-ops nil)
(ebox--constraint-change-report-props change)
(list :viewport-axes axes
:host-context-changed display-changed
:target-viewport-width viewport-width
:target-viewport-height target-height))
:state-overrides
(list :viewport-width viewport-width
:viewport-height target-height
:display-signature display-signature))
(let* ((candidate-scroll-table
(ebox-incremental--candidate-scroll-state-table
buffer old-state old-state
(plist-get old-state :region-box-table)))
(candidate-state
(ebox-incremental--isolate-candidate-caches
(copy-sequence old-state)))
(prepared
(list :root (plist-get old-state :root-node)
:dirty-set dirty-set
:display-signature display-signature
:scroll-state-table candidate-scroll-table))
(report-overrides
(append (ebox--constraint-change-report-props change)
(list :viewport-axes axes
:host-context-changed display-changed
:target-viewport-width viewport-width
:target-viewport-height target-height)))
owner-plan report projection-kind scroll-partition
viewport-reflow-safe-p native-continuity-p)
(plist-put candidate-state :viewport-width viewport-width)
(plist-put candidate-state :viewport-height target-height)
(plist-put candidate-state :display-signature display-signature)
;; Fragment reuse is a consequence of the complete viewport-reflow
;; proof below, not a proxy for "the display did not change". An
;; owner-rerender can still change intrinsic measurements, scroll
;; dependencies, and generation-sensitive side effects.
(plist-put candidate-state :layout-fragments-reuse-p nil)
(plist-put candidate-state :runtime-revision
(1+ (or (plist-get old-state :runtime-revision) 0)))
(plist-put candidate-state :region-box-table
(copy-hash-table
(plist-get old-state :region-box-table)))
(plist-put candidate-state :scroll-state-table candidate-scroll-table)
(setq owner-plan
(ebox-incremental--layout-owner-plan
buffer old-state candidate-state dirty-set)
report
(ebox-incremental--plist-overlay
(ebox-incremental--layout-owner-report
prepared owner-plan candidate-state)
report-overrides))
(setq viewport-reflow-safe-p
(and (not display-changed)
(ebox-incremental--viewport-reflow-safe-p
old-state candidate-state dirty-set owner-plan
style-context-stable-p)))
(setq native-continuity-p
(and viewport-reflow-safe-p
(require 'ebox-native-commit nil t)
(ebox-native-commit-prepare-viewport-continuity
old-state candidate-state)))
(setq projection-kind
(cond
(native-continuity-p 'native-frame)
(viewport-reflow-safe-p
'viewport-reflow)
((and (not display-changed)
(setq scroll-partition
(ebox-incremental--viewport-scroll-partition
candidate-state))
(ebox-incremental--viewport-mixed-scroll-proof-p
old-state candidate-state dirty-set owner-plan
scroll-partition style-context-stable-p))
'viewport-reflow-mixed-scroll)))
(when native-continuity-p
(plist-put candidate-state :native-topology-stable-p t)
(plist-put candidate-state :native-touched-node-ids nil)
(plist-put candidate-state :native-removed-node-ids nil)
(setq owner-plan
(ebox-incremental--native-frame-plan buffer dirty-set)
report
(ebox-incremental--plist-overlay
(ebox-incremental--layout-owner-report
prepared owner-plan candidate-state)
report-overrides)))
(when (memq projection-kind
'(viewport-reflow viewport-reflow-mixed-scroll))
(plist-put candidate-state :layout-fragments-reuse-p t))
(when projection-kind
(setq report (plist-put report :projection-kind projection-kind))
(setq report
(plist-put report :viewport-style-context-stable-p
style-context-stable-p))
(when scroll-partition
(setq report
(plist-put report :viewport-scroll-stable-ids
(plist-get scroll-partition :stable)))
(setq report
(plist-put report :viewport-scroll-affected-ids
(plist-get scroll-partition :affected)))))
(list :root (plist-get candidate-state :root-node)
:scope-node-ids
(delete-dups
(or (mapcar (lambda (op) (plist-get op :owner-id)) owner-plan)
(mapcar (lambda (entry) (plist-get entry :node-id))
dirty-set)))
:report-base report
:projection-kind projection-kind
:state-overrides
(cond
((eq projection-kind 'native-frame)
(append
(ebox-incremental--viewport-state-overrides
candidate-state t)
(list :native-topology-stable-p t
:native-touched-node-ids nil
:native-removed-node-ids nil)))
((memq projection-kind
'(viewport-reflow viewport-reflow-mixed-scroll))
(ebox-incremental--viewport-reflow-state-overrides
old-state candidate-state
(and (eq projection-kind 'viewport-reflow-mixed-scroll)
scroll-partition)))
(t
(ebox-incremental--viewport-state-overrides
candidate-state))))))))
(defun ebox-incremental--finalize-declarative-scroll-publication
(scroll-keys &optional prefetch-delay)
"Retire stale scroll work and resume committed SCROLL-KEYS.
PREFETCH-DELAY is forwarded when a live, inactive scroll region needs warming."
(let (diagnostics)
(cl-labels
((run (region-id action function)
(let ((inhibit-quit t)
(quit-flag nil))
(condition-case failure
(funcall function)
((error quit)
(push (list :region-id region-id :phase 'scroll-finalization
:action action
:condition failure)
diagnostics))))))
(dolist (region-id scroll-keys)
(run region-id 'cancel-prefetch
(lambda () (ebox--scroll-cancel-idle-prefetch region-id)))
(if (gethash region-id ebox--scroll-global-state)
(unless (gethash region-id ebox--smooth-scroll-state-table)
(run region-id 'schedule-prefetch
(lambda ()
(ebox--scroll-schedule-idle-prefetch
region-id prefetch-delay))))
(run region-id 'stop-smooth-scroll
(lambda () (ebox--smooth-scroll-stop region-id))))))
(nreverse diagnostics)))
(defun ebox-incremental--commit-report (prepared patch-report state)
"Return the declarative report for PREPARED, PATCH-REPORT, and STATE."
(let* ((dirty-set (plist-get prepared :dirty-set))
(report
(or (and patch-report (copy-sequence patch-report))
(ebox--update-report
nil 'no-op :dirty-count 0 :patch-count 0 :patch-ops nil))))
(setq report (plist-put report :constraint-source 'declarative))
(setq report
(plist-put report :constraint-root-node-id
(plist-get (plist-get prepared :root) :node-id)))
(setq report (plist-put report :dirty-count (length dirty-set)))
(setq report
(plist-put report :dirty-kinds (ebox--dirty-set-kinds dirty-set)))
(setq report
(plist-put report :dirty-keys
(ebox--dirty-set-changed-keys dirty-set)))
(setq report
(plist-put report :render-scope
(if patch-report 'dirty-owners 'none)))
(setq report
(plist-put report :rendered-owner-ids
(copy-sequence (plist-get report :owner-ids))))
(setq report
(plist-put report :publication-scope
(if patch-report
(or (plist-get report :publication-scope)
'minimal-spans)
'runtime-only)))
(setq report (plist-put report :runtime-published t))
(setq report
(plist-put report :runtime-revision
(plist-get state :runtime-revision)))
report))
(provide 'ebox-incremental)
;;; ebox-incremental.el ends here