Optimize incremental overlay layer recomposition
Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run
Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run
This commit is contained in:
parent
4d0d46d5be
commit
01b1a86bb3
@ -10452,7 +10452,14 @@ role, and outside-complement compatibility before publication."
|
||||
(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))
|
||||
:scroll-state-table (plist-get prepared :scroll-state-table)
|
||||
:previous-layer-composition-cache
|
||||
(plist-get old-state :layer-composition-cache)
|
||||
:layer-composition-cache (make-hash-table :test 'equal)
|
||||
:layer-composition-context
|
||||
(list (plist-get candidate-state :viewport-width)
|
||||
(plist-get candidate-state :viewport-height)
|
||||
(plist-get prepared :display-signature)))
|
||||
(ebox-incremental--native-runtime-overrides candidate-state)
|
||||
(list :source-path-copied-p source-path-copied-p
|
||||
:runtime-index-prepared-p
|
||||
@ -11279,6 +11286,13 @@ When RETAIN-NATIVE-P is non-nil, preserve its committed native frame bases."
|
||||
:layout-fragments-reuse-p
|
||||
(plist-get state :layout-fragments-reuse-p)
|
||||
:render-signature-cache (plist-get state :render-signature-cache)
|
||||
:previous-layer-composition-cache
|
||||
(plist-get state :layer-composition-cache)
|
||||
:layer-composition-cache (make-hash-table :test 'equal)
|
||||
:layer-composition-context
|
||||
(list (plist-get state :viewport-width)
|
||||
(plist-get state :viewport-height)
|
||||
(plist-get state :display-signature))
|
||||
:flex-content-min-widths (plist-get state :flex-content-min-widths)
|
||||
:viewport-height-dependent-subtree-cache
|
||||
(plist-get state :viewport-height-dependent-subtree-cache)
|
||||
|
||||
105
ebox-layer.el
105
ebox-layer.el
@ -44,6 +44,27 @@ Keys are source strings by identity. Never retained between renders.")
|
||||
Retained publication supplies a known boolean; isolated rendering resolves
|
||||
the fact once for its spatial root.")
|
||||
|
||||
(defvar ebox-layer--previous-composition-cache nil
|
||||
"Read-only retained host composition ledger for the current candidate.
|
||||
The ledger is an opaque hash table owned by the Surface runtime. Entries are
|
||||
keyed by host identity and composition phase and contain immutable row
|
||||
signatures plus their already composed rows. A missing ledger disables reuse.")
|
||||
|
||||
(defvar ebox-layer--composition-cache nil
|
||||
"Candidate-local host composition ledger populated during rendering.
|
||||
Callers should bind this to a fresh hash table for one candidate; this module
|
||||
never mutates `ebox-layer--previous-composition-cache'. Use an `equal' hash
|
||||
table because ledger keys are freshly allocated HOST/PHASE cons cells.")
|
||||
|
||||
(defvar ebox-layer--composition-context nil
|
||||
"Immutable context token for host composition reuse.
|
||||
The Surface runtime should include every display and allocation fact that can
|
||||
change pixel measurement. Reuse is disabled when this token is nil.")
|
||||
|
||||
(defvar ebox-layer--composition-phase 'final
|
||||
"Composition phase used as the default host ledger key.
|
||||
Anchor prepasses and final paints must use distinct phase values.")
|
||||
|
||||
(defun ebox-layer-active-p (&optional node)
|
||||
"Return the current candidate's layer capability without repeated scans."
|
||||
(if (eq ebox-layer--active-p 'unknown)
|
||||
@ -214,15 +235,82 @@ Nested scroll changes and source updates cannot use this root-only proof."
|
||||
(not (ebox-layer-positioned-p an)))
|
||||
(t (< (gethash an order 0) (gethash bn order 0)))))))))
|
||||
|
||||
(defun ebox-layer--compose-rows (host records width height)
|
||||
"Compose HOST's visible RECORDS in a WIDTH by HEIGHT content canvas."
|
||||
(let ((rows (make-vector height nil)))
|
||||
(dolist (record (ebox-layer--ordered-records host records))
|
||||
(defun ebox-layer--stable-node-id (node)
|
||||
"Return NODE's retained identity, or nil when it cannot be proven stable."
|
||||
(or (plist-get node :node-id)
|
||||
(and ebox--render-source-index
|
||||
(ebox-tree-node-id ebox--render-source-index node))))
|
||||
|
||||
(defun ebox-layer--record-signature (record)
|
||||
"Return immutable comparison facts for one paint RECORD.
|
||||
Do not retain NODE itself: copied candidate nodes can have mutable transient
|
||||
attachments. The sliced text is compared with its complete text properties,
|
||||
which captures display geometry, paint and interaction values."
|
||||
(let ((node (plist-get record :node)))
|
||||
(list :node-id (ebox-layer--stable-node-id node)
|
||||
:x (plist-get record :x)
|
||||
:y (plist-get record :y)
|
||||
:z-index (or (plist-get node :z-index) 0)
|
||||
:positioned (and (ebox-layer-positioned-p node) t)
|
||||
:text (plist-get record :text))))
|
||||
|
||||
(defun ebox-layer--compose-cache-key (host phase)
|
||||
"Return the retained-ledger key for HOST and composition PHASE.
|
||||
Use the retained node id so copied candidate nodes can reuse their prior
|
||||
host's rows; an unidentifiable host disables the cache."
|
||||
(when-let* ((id (ebox-layer--stable-node-id host)))
|
||||
(cons id phase)))
|
||||
|
||||
(defun ebox-layer--compose-rows (host records width height &optional phase)
|
||||
"Compose HOST's visible RECORDS in a WIDTH by HEIGHT content canvas.
|
||||
When the caller binds the retained and candidate ledgers and an immutable
|
||||
composition context, rows whose ordered record signatures are unchanged are
|
||||
returned directly without calling `ebox-composite-line'. PHASE separates
|
||||
anchor prepasses from final composition so neither can reuse the other."
|
||||
(let* ((phase (or phase ebox-layer--composition-phase))
|
||||
(ordered (ebox-layer--ordered-records host records))
|
||||
(rows (make-vector height nil))
|
||||
(signatures (make-vector height nil))
|
||||
(key (ebox-layer--compose-cache-key host phase))
|
||||
(previous (and ebox-layer--composition-context
|
||||
(hash-table-p ebox-layer--previous-composition-cache)
|
||||
(gethash key ebox-layer--previous-composition-cache)))
|
||||
(old-context (and previous (plist-get previous :context)))
|
||||
(old-signatures (and previous (plist-get previous :signatures)))
|
||||
(old-rows (and previous (plist-get previous :rows)))
|
||||
(cacheable-p (and key ebox-layer--composition-context
|
||||
(hash-table-p ebox-layer--composition-cache))))
|
||||
(dolist (record ordered)
|
||||
(let ((row (plist-get record :y)))
|
||||
(when (and (>= row 0) (< row height))
|
||||
(when (and (integerp row) (>= row 0) (< row height))
|
||||
(push record (aref rows row)))))
|
||||
(cl-loop for row across rows collect
|
||||
(ebox-composite-line "" (nreverse row) width))))
|
||||
(dotimes (row height)
|
||||
(let* ((placements (nreverse (aref rows row)))
|
||||
(signature (mapcar #'ebox-layer--record-signature placements))
|
||||
(reusable-p
|
||||
(and cacheable-p previous
|
||||
(equal old-context ebox-layer--composition-context)
|
||||
(= width (plist-get previous :width))
|
||||
(= height (plist-get previous :height))
|
||||
(vectorp old-signatures) (vectorp old-rows)
|
||||
(= (length old-rows) height)
|
||||
(cl-every (lambda (entry)
|
||||
(plist-get entry :node-id))
|
||||
signature)
|
||||
(= (length old-signatures) height)
|
||||
(equal-including-properties
|
||||
signature (aref old-signatures row)))))
|
||||
(aset signatures row signature)
|
||||
(aset rows row
|
||||
(if reusable-p
|
||||
(aref old-rows row)
|
||||
(ebox-composite-line "" placements width)))))
|
||||
(when cacheable-p
|
||||
(puthash key (list :context ebox-layer--composition-context
|
||||
:width width :height height
|
||||
:signatures signatures :rows rows)
|
||||
ebox-layer--composition-cache))
|
||||
(append rows nil)))
|
||||
|
||||
(defun ebox-layer--anchor-node (host id)
|
||||
"Find one logical anchor ID under HOST, rejecting ambiguous IDs."
|
||||
@ -393,7 +481,8 @@ Only candidate-local lists of visible paint records are sorted."
|
||||
;; Anchor geometry follows relative movement and visible clipping,
|
||||
;; not the original flow slot. This extra composition is needed
|
||||
;; only by anchored panels.
|
||||
(setq lines (ebox-layer--compose-rows host records width height)))
|
||||
(setq lines (ebox-layer--compose-rows host records width height
|
||||
'anchor-prepass)))
|
||||
(setq records (nconc (ebox-layer--absolute-records host absolute lines width height)
|
||||
records))
|
||||
(ebox--record-rendered-intrinsic-size
|
||||
|
||||
@ -319,6 +319,12 @@ rather than relying on an implicit default.")
|
||||
(:symbol ebox-layer--piece-width-cache
|
||||
:reason dynamically-bound-composition-index
|
||||
:evidence let-bound-per-layer-host-and-never-retained-between-renders)
|
||||
(:symbol ebox-layer--composition-cache
|
||||
:reason dynamically-bound-candidate-composition-ledger
|
||||
:evidence candidate-local hash table is published through the render state plist)
|
||||
(:symbol ebox-layer--previous-composition-cache
|
||||
:reason dynamically-bound-committed-composition-ledger
|
||||
:evidence read-only prior ledger supplied by committed render state and discarded on replacement)
|
||||
(:symbol ebox--string-pixel-width-cache-ring-index
|
||||
:reason numeric-eviction-cursor
|
||||
:evidence value-is-an-integer-ring-position)
|
||||
|
||||
187
ebox-surface.el
187
ebox-surface.el
@ -44,6 +44,9 @@
|
||||
(defvar ebox--render-root-parent-kind)
|
||||
(defvar ebox--render-cache-table)
|
||||
(defvar ebox--render-cache-signature-cache)
|
||||
(defvar ebox-layer--previous-composition-cache)
|
||||
(defvar ebox-layer--composition-cache)
|
||||
(defvar ebox-layer--composition-context)
|
||||
(defvar ebox--layout-fragments-table)
|
||||
(defvar ebox--layout-fragments-reuse-p)
|
||||
(defvar ebox--flex-content-min-width-table)
|
||||
@ -1313,7 +1316,15 @@ FRAMEWORK-PARTICIPANT owns paired framework publication when non-nil."
|
||||
(with-current-buffer buffer
|
||||
(setq-local ebox-surface--buffer-surface surface))
|
||||
(if new-state
|
||||
(puthash buffer new-state ebox--buffer-render-state-table)
|
||||
(progn
|
||||
;; Preserve the committed layer ledger for the next candidate.
|
||||
;; The renderer owns the fresh candidate cache; this fallback
|
||||
;; keeps it alive across publication paths that do not render.
|
||||
(when (and old-mirror
|
||||
(null (plist-get new-state :layer-composition-cache)))
|
||||
(plist-put new-state :layer-composition-cache
|
||||
(plist-get old-mirror :layer-composition-cache)))
|
||||
(puthash buffer new-state ebox--buffer-render-state-table))
|
||||
(remhash buffer ebox--buffer-render-state-table))
|
||||
(ebox-incremental--replace-hash-entries
|
||||
ebox--region-box-table region-keys
|
||||
@ -1715,7 +1726,14 @@ PREVIOUS-STATE's node-object table."
|
||||
:style-binding-states style-state-table
|
||||
:stylesheet-signature stylesheet-signature
|
||||
:selector-tree-snapshot selector-tree-snapshot
|
||||
:selector-tree-token selector-tree-token)))
|
||||
:selector-tree-token selector-tree-token
|
||||
:previous-layer-composition-cache
|
||||
(plist-get previous-state :layer-composition-cache)
|
||||
:layer-composition-cache (make-hash-table :test 'equal)
|
||||
:layer-composition-context
|
||||
(list (plist-get state-overrides :viewport-width)
|
||||
(plist-get state-overrides :viewport-height)
|
||||
(plist-get state-overrides :display-signature)))))
|
||||
|
||||
(defun ebox-surface--project-stable-node-table
|
||||
(context nodes node-root old-objects objects-by-node)
|
||||
@ -3306,8 +3324,9 @@ available during planning; replacements here belong only to the candidate."
|
||||
(ebox-surface--refresh-rendered-layout-snapshots state)
|
||||
rendered))
|
||||
|
||||
(defun ebox-surface--render-candidate-node (state node)
|
||||
"Render NODE from candidate STATE in isolated Ebox side tables."
|
||||
(defun ebox-surface--render-candidate-node (state node &optional complete-lines-p)
|
||||
"Render NODE from candidate STATE in isolated Ebox side tables.
|
||||
COMPLETE-LINES-P requires a proven physical line origin for every output row."
|
||||
(let ((scroll-table
|
||||
(or (plist-get state :scroll-state-table)
|
||||
(make-hash-table :test 'equal)))
|
||||
@ -3351,6 +3370,18 @@ available during planning; replacements here belong only to the candidate."
|
||||
(plist-get state :layout-fragments-reuse-p))
|
||||
(ebox--render-cache-signature-cache
|
||||
(plist-get state :render-signature-cache))
|
||||
;; Layer composition is a candidate-local ledger. Previous rows
|
||||
;; are read-only; the fresh table is retained only after commit.
|
||||
(ebox-layer--previous-composition-cache
|
||||
(plist-get state :previous-layer-composition-cache))
|
||||
(ebox-layer--composition-cache
|
||||
(or (plist-get state :layer-composition-cache)
|
||||
(make-hash-table :test 'equal)))
|
||||
(ebox-layer--composition-context
|
||||
(or (plist-get state :layer-composition-context)
|
||||
(list (plist-get state :viewport-width)
|
||||
(plist-get state :viewport-height)
|
||||
(plist-get state :display-signature))))
|
||||
(ebox--scroll-window-initial-lookahead-lines-override
|
||||
(ebox-surface--retained-scroll-lookahead state))
|
||||
(ebox--viewport-dependent-node-ids-cache
|
||||
@ -3374,6 +3405,12 @@ available during planning; replacements here belong only to the candidate."
|
||||
(ebox-native-commit-render state node))
|
||||
(let ((ebox--surface-materialization-active t))
|
||||
(ebox--render-layout node)))))
|
||||
(when (and complete-lines-p
|
||||
(ebox--fractional-pixel-spaces-p rendered))
|
||||
;; Untouched buffer rows are already quantized. Their raw
|
||||
;; fractional evidence cannot be disproved by this local render.
|
||||
(plist-put state :fractional-pixel-output-p t)
|
||||
(setq rendered (ebox--quantize-pixel-spaces rendered)))
|
||||
(ebox-surface--check-local-pixel-output rendered)
|
||||
(unless (plist-get state :native-render-p)
|
||||
(ebox--record-render-output-provenance rendered))
|
||||
@ -3385,6 +3422,8 @@ available during planning; replacements here belong only to the candidate."
|
||||
(ebox-surface--hash-keys scroll-table))
|
||||
(plist-put state :render-owned-text-values
|
||||
ebox--render-owned-text-values)
|
||||
(plist-put state :layer-composition-cache
|
||||
ebox-layer--composition-cache)
|
||||
rendered)))))
|
||||
|
||||
(defun ebox-surface--attach-retained-node-objects (state)
|
||||
@ -3499,6 +3538,24 @@ old allocated width while the owner changes its natural content length."
|
||||
(push (substring source (- cursor origin)) pieces)
|
||||
(apply #'concat (nreverse pieces)))))
|
||||
|
||||
(defun ebox-surface--changed-span-pairs (source origin pairs)
|
||||
"Return only changed SOURCE/replacement PAIRS.
|
||||
PAIRS contains buffer-position spans paired with candidate replacement lines.
|
||||
Compare both characters and text properties, so a row whose composed pixels
|
||||
and ownership are unchanged is omitted from the edit set. Keeping unchanged
|
||||
rows out of the transaction is the damage-region optimization for layer moves:
|
||||
the candidate may still be rendered for proof, but publication touches only
|
||||
the old/new affected rows."
|
||||
(cl-remove-if
|
||||
(lambda (pair)
|
||||
(let* ((span (car pair))
|
||||
(line (cdr pair))
|
||||
(start (- (car span) origin))
|
||||
(end (- (cdr span) origin)))
|
||||
(and (<= 0 start end (length source))
|
||||
(equal-including-properties (substring source start end) line))))
|
||||
pairs))
|
||||
|
||||
(defun ebox-surface--span-patch-details
|
||||
(_buffer snapshot node spans rendered
|
||||
&optional variable-content-p variable-content-max-width)
|
||||
@ -4260,7 +4317,8 @@ Render STATE's candidate into BUFFER's retained owner allocation."
|
||||
(t node)))
|
||||
(rendered
|
||||
(and render-node
|
||||
(ebox-surface--render-candidate-node state render-node)))
|
||||
(ebox-surface--render-candidate-node
|
||||
state render-node (plist-get proof :complete-physical-lines-p))))
|
||||
(rendered
|
||||
(if (and rendered (plist-get proof :nested-scroll-patch-p))
|
||||
(ebox-surface--nested-scroll-owned-lines
|
||||
@ -4285,7 +4343,9 @@ Render STATE's candidate into BUFFER's retained owner allocation."
|
||||
(and rendered
|
||||
(or (ebox-buffer--rendered-in-existing-slots
|
||||
spans rendered nil t)
|
||||
(and (plist-get proof :scroll-content-coordinate-p)
|
||||
(and (or (plist-get proof :scroll-content-coordinate-p)
|
||||
(plist-get proof :complete-physical-lines-p))
|
||||
(= (length spans) (length (ebox-string-lines rendered)))
|
||||
(cl-every #'ebox--span-whole-line-p spans)
|
||||
rendered))))
|
||||
(lines (and replacement (ebox-string-lines replacement)))
|
||||
@ -4564,11 +4624,85 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
||||
(cl-remf state :scroll-content-proof))))
|
||||
output)))
|
||||
|
||||
(defun ebox-surface--span-patch-output (buffer previous-state state)
|
||||
"Return a proven local patch in BUFFER or its retained scroll content."
|
||||
(if (plist-get state :scroll-content-proof)
|
||||
(ebox-surface--scroll-content-patch-output buffer previous-state state)
|
||||
(ebox-surface--buffer-span-patch-output buffer previous-state state)))
|
||||
(defun ebox-surface--complete-line-allocation-p (state proof)
|
||||
"Prove PROOF's detached slot preserves STATE's raw containing width.
|
||||
Published pixels have already been rounded. Restrict this proof to the root
|
||||
and its ordinary flow/Column children, with the same containing viewport."
|
||||
(let* ((root (plist-get state :root-node))
|
||||
(root-id (plist-get root :node-id))
|
||||
(owner-id (plist-get proof :owner-id))
|
||||
(node (ebox-runtime-index-get owner-id (plist-get state :node-table)))
|
||||
(width (plist-get
|
||||
(plist-get (plist-get proof :snapshot) :external-footprint-signature)
|
||||
:max-line-pixel-width))
|
||||
(ebox-viewport-width (plist-get state :viewport-width))
|
||||
(ebox-viewport-height (plist-get state :viewport-height)))
|
||||
(and node (numberp width)
|
||||
(memq (ebox-tree-display-inner root) '(flow column))
|
||||
(or (equal owner-id root-id)
|
||||
(and (equal (ebox-runtime-index-get
|
||||
owner-id (plist-get state :parent-table)) root-id)
|
||||
(eq (ebox-tree-display-outer node) 'block)
|
||||
(memq (ebox-get node :position) '(nil static))))
|
||||
(cl-every
|
||||
(lambda (box)
|
||||
(cl-every (lambda (value)
|
||||
(or (memq value '(nil auto stretch none))
|
||||
(numberp value) (ebox-size-value-p value)))
|
||||
(mapcar (lambda (key) (ebox-get box key))
|
||||
'(:width :min-width :max-width))))
|
||||
(list root node))
|
||||
(let ((root-width (ebox--wrapper-content-viewport-pixel root)))
|
||||
(and (numberp root-width) (numberp ebox-viewport-width)
|
||||
(= root-width ebox-viewport-width)
|
||||
(let ((raw-width
|
||||
(if (equal owner-id root-id)
|
||||
(+ root-width (ebox--side-pixel root))
|
||||
(ebox--call-with-box-content-context
|
||||
root
|
||||
(lambda ()
|
||||
(when-let* ((content (ebox--wrapper-content-viewport-pixel node)))
|
||||
(+ content (ebox--side-pixel node))))))))
|
||||
(and (numberp raw-width) (= raw-width width))))))))
|
||||
|
||||
(defun ebox-surface--span-patch-output
|
||||
(buffer previous-state state &optional complete-lines-only-p)
|
||||
"Return a proven local patch in BUFFER or its retained scroll content.
|
||||
COMPLETE-LINES-ONLY-P admits layer composition only at complete physical rows;
|
||||
detached horizontal owners keep the ordinary fractional-origin fallback."
|
||||
(if complete-lines-only-p
|
||||
(let ((proofs (plist-get state :owner-scoped-proofs)))
|
||||
(when (and proofs
|
||||
(not (plist-get state :scroll-content-proof))
|
||||
(cl-some (lambda (proof) (plist-get proof :layer-recompose-p))
|
||||
proofs)
|
||||
(with-current-buffer buffer
|
||||
(save-restriction
|
||||
(widen)
|
||||
(cl-every
|
||||
(lambda (proof)
|
||||
(let ((spans (plist-get (plist-get proof :snapshot)
|
||||
:buffer-spans)))
|
||||
(and spans
|
||||
(ebox-surface--complete-line-allocation-p state proof)
|
||||
(not (plist-get proof :scroll-content-coordinate-p))
|
||||
(not (plist-get proof :nested-scroll-patch-p))
|
||||
(not (plist-get proof :range-splice-p))
|
||||
(cl-every #'ebox--span-whole-line-p spans))))
|
||||
proofs))))
|
||||
(plist-put state :owner-scoped-proofs
|
||||
(mapcar (lambda (proof)
|
||||
(if (plist-get proof :layer-recompose-p)
|
||||
(plist-put (copy-sequence proof)
|
||||
:complete-physical-lines-p t)
|
||||
proof))
|
||||
proofs))
|
||||
(let ((ebox-surface--integral-local-output-required-p t))
|
||||
(catch 'ebox/fractional-local-output
|
||||
(ebox-surface--buffer-span-patch-output buffer previous-state state)))))
|
||||
(if (plist-get state :scroll-content-proof)
|
||||
(ebox-surface--scroll-content-patch-output buffer previous-state state)
|
||||
(ebox-surface--buffer-span-patch-output buffer previous-state state))))
|
||||
|
||||
(defun ebox-surface--buffer-span-patch-output (buffer previous-state state)
|
||||
"Return a proven single- or multi-owner local patch output, or nil."
|
||||
@ -4691,10 +4825,22 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
||||
(sort pairs
|
||||
(lambda (left right)
|
||||
(< (car (car left)) (car (car right))))))
|
||||
;; A layer owner is rendered as a complete set of rows so that
|
||||
;; occlusion can be proven correctly. Publishing that candidate
|
||||
;; need only touch rows whose bytes or properties actually
|
||||
;; changed; unchanged rows remain mounted in place and therefore
|
||||
;; do not participate in the TP edit or fragment rebuild.
|
||||
(setq pairs (ebox-surface--changed-span-pairs source origin pairs))
|
||||
;; The paint-origin ledger restores hidden provenance on retained
|
||||
;; rows before this point. Rows that are then byte-and-property
|
||||
;; identical need no buffer splice or fragment reconstruction;
|
||||
;; keep their old carriers in place for a future expose operation.
|
||||
(when-let* ((output
|
||||
(ebox-surface--replace-buffer-spans
|
||||
source origin (mapcar #'car pairs)
|
||||
(mapcar #'cdr pairs))))
|
||||
(if pairs
|
||||
(ebox-surface--replace-buffer-spans
|
||||
source origin (mapcar #'car pairs)
|
||||
(mapcar #'cdr pairs))
|
||||
source)))
|
||||
(plist-put state :content-base-extent (length source))
|
||||
(plist-put
|
||||
state :content-coordinate-patches
|
||||
@ -4731,7 +4877,12 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
||||
(ebox-surface--materialized-fragment-ledger
|
||||
previous-state)))
|
||||
(fragments
|
||||
(if retained-properties-p
|
||||
(if (null pairs)
|
||||
;; No visible row changed. Keep the exact
|
||||
;; fragment ledger object and avoid scanning or
|
||||
;; rebasing any text properties.
|
||||
old-fragments
|
||||
(if retained-properties-p
|
||||
(if (and (not coordinate-shift-p)
|
||||
(eq old-fragments
|
||||
(plist-get previous-state
|
||||
@ -4745,7 +4896,7 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
||||
(ebox-surface--incremental-patched-fragments
|
||||
output old-fragments
|
||||
(plist-get state :content-coordinate-patches)
|
||||
state))))
|
||||
state)))))
|
||||
(plist-put state :span-patch-fragment-data fragments)
|
||||
(plist-put state :span-patch-content-p t)
|
||||
(when (and retained-properties-p (not coordinate-shift-p))
|
||||
@ -7120,6 +7271,10 @@ published runtime and must be consumed without clearing those shared nodes."
|
||||
(not (plist-get state :native-scroll-materialize-p))
|
||||
(ebox-surface--scroll-patch-output
|
||||
(ebox-surface--signals-buffer signals) state t))
|
||||
(and (memq projection-kind '(span-patch owner-scoped))
|
||||
(ebox-surface--span-patch-output
|
||||
(ebox-surface--signals-buffer signals)
|
||||
previous-state state t))
|
||||
;; A detached fractional owner cannot recover
|
||||
;; the remainder spent by preceding siblings.
|
||||
(unless (plist-get previous-state
|
||||
|
||||
@ -14600,4 +14600,5 @@ face patch must match that exactly (issue014)."
|
||||
"#674795"))
|
||||
(should (ebox-test--buffer-propertized-matches-runtime-render-p)))))
|
||||
|
||||
|
||||
;;; ebox-core-render-tests.el ends here
|
||||
|
||||
@ -179,6 +179,184 @@
|
||||
(ebox-css-layout-test--display
|
||||
(ebox-render next)))))))))))
|
||||
|
||||
(defun ebox-css-layout-test--fractional-layer (&optional partial left)
|
||||
"Build a layer at LEFT with a disjoint status; PARTIAL shares its rows."
|
||||
(let ((canvas
|
||||
`(box :id canvas :height (lh 6) :background-color "#112233"
|
||||
(box :id card :position absolute :left ,(or left '(% 15)) :top (lh 1)
|
||||
:width (px 35) :height (lh 3) :border "red"
|
||||
:background-color "#334455" :help-echo "Moving card"
|
||||
:pointer hand :keymap (keymap (13 . ignore))
|
||||
"CARD"))))
|
||||
(ebox-build
|
||||
`(column :width (px 101)
|
||||
(box :height (lh 1) "Header")
|
||||
,(if partial
|
||||
`(row (box :width (% 50) ,canvas)
|
||||
(box :width (% 50) "Sibling"))
|
||||
canvas)
|
||||
(box :id status :height (lh 1) "0")))))
|
||||
|
||||
(defun ebox-css-layout-test--assert-fresh-properties ()
|
||||
"Compare retained geometry, paint and interactions with a fresh render."
|
||||
(let ((actual (buffer-string))
|
||||
(fresh (ebox-render
|
||||
(plist-get (ebox-surface-buffer-snapshot (current-buffer))
|
||||
:input))))
|
||||
(should (equal (substring-no-properties actual)
|
||||
(substring-no-properties fresh)))
|
||||
(dotimes (position (length actual))
|
||||
(dolist (property '(face font-lock-face display help-echo keymap
|
||||
mouse-face pointer))
|
||||
(ert-info ((format "Property %S at character %d" property position))
|
||||
(should (equal (get-text-property position property actual)
|
||||
(get-text-property position property fresh))))))))
|
||||
|
||||
(ert-deftest ebox-css-layout-fractional-layer-moves-in-complete-rows ()
|
||||
"Complete-row composition keeps local updates and per-line pixel origins."
|
||||
(ebox-css-layout-test--with-pixel-display
|
||||
(let ((ebox-viewport-width 101) (ebox-viewport-height 8))
|
||||
(with-temp-buffer
|
||||
(ebox-render-to-buffer
|
||||
(current-buffer) (ebox-css-layout-test--fractional-layer))
|
||||
(should (plist-get (ebox--buffer-render-state (current-buffer))
|
||||
:fractional-pixel-output-p))
|
||||
(dolist (position '(16.5 4.5 -3.5 15.15))
|
||||
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||
(lambda (&rest _) (ert-fail "Complete layer rerendered root"))))
|
||||
(ebox-call-with-update-batch
|
||||
(lambda ()
|
||||
(ebox-region-update 'card :left `(px ,position))
|
||||
(ebox-region-update 'status :content (format "%s" position)))))
|
||||
(ebox-css-layout-test--assert-pixel-lines (buffer-string) 101)
|
||||
(should
|
||||
(equal (ebox-css-layout-test--display (buffer-string))
|
||||
(ebox-css-layout-test--display
|
||||
(ebox-render (plist-get (ebox-surface-buffer-snapshot
|
||||
(current-buffer)) :input))))))))))
|
||||
|
||||
(ert-deftest ebox-css-layout-fractional-layer-keeps-partial-row-fallback ()
|
||||
"A layer sharing a physical row cannot discard its sibling's remainder."
|
||||
(ebox-css-layout-test--with-pixel-display
|
||||
(let ((ebox-viewport-width 101) (ebox-viewport-height 8))
|
||||
(with-temp-buffer
|
||||
(ebox-render-to-buffer
|
||||
(current-buffer) (ebox-css-layout-test--fractional-layer t))
|
||||
(let ((render (symbol-function 'ebox-surface--render-candidate)) (calls 0))
|
||||
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||
(lambda (&rest args) (cl-incf calls) (apply render args))))
|
||||
(ebox-region-update 'card :left '(px 16.5)))
|
||||
(should (> calls 0)))
|
||||
(should
|
||||
(equal (ebox-css-layout-test--display (buffer-string))
|
||||
(ebox-css-layout-test--display
|
||||
(ebox-render (plist-get (ebox-surface-buffer-snapshot
|
||||
(current-buffer)) :input)))))))))
|
||||
|
||||
(ert-deftest ebox-css-layout-fractional-layer-keeps-raw-allocation-fallback ()
|
||||
"Whole rows preserve both unrounded widths and the containing CSS basis."
|
||||
(ebox-css-layout-test--with-pixel-display
|
||||
(let ((ebox-viewport-width 101) (ebox-viewport-height 4))
|
||||
(dolist (input
|
||||
'((column :width (px 100.5)
|
||||
(box :id host :width (% 100) :height (lh 2)
|
||||
(box :id card :position absolute :left (% 67.7)
|
||||
:width (px 7) :height (lh 1) "X")))
|
||||
;; Published and raw widths both equal 150 here. Detached
|
||||
;; padding must still resolve against 150, not viewport 101.
|
||||
(column :width (px 150)
|
||||
(box :id host :width (% 100) :padding-inline (% 10)
|
||||
:height (lh 2)
|
||||
(box :id card :position absolute :left (px 0)
|
||||
:width (px 7) :height (lh 1) "X")))))
|
||||
(ert-info ((format "Containing width %S" (plist-get (cdr input) :width)))
|
||||
(with-temp-buffer
|
||||
(ebox-render-to-buffer (current-buffer) (ebox-build input))
|
||||
(let ((render (symbol-function 'ebox-surface--render-candidate)) (calls 0))
|
||||
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||
(lambda (&rest args) (cl-incf calls) (apply render args))))
|
||||
(ebox-region-update 'card :top '(lh 1)))
|
||||
(should (> calls 0)))
|
||||
(ebox-css-layout-test--assert-fresh-properties)))))))
|
||||
|
||||
(ert-deftest ebox-css-layout-fractional-layer-retains-new-raw-fraction-flag ()
|
||||
"Local layers retain newly observed raw fractions after integral movement."
|
||||
(ebox-css-layout-test--with-pixel-display
|
||||
(let ((ebox-viewport-width 101) (ebox-viewport-height 8))
|
||||
(with-temp-buffer
|
||||
(ebox-render-to-buffer
|
||||
(current-buffer) (ebox-css-layout-test--fractional-layer nil '(px 0)))
|
||||
(should-not (plist-get (ebox--buffer-render-state (current-buffer))
|
||||
:fractional-pixel-output-p))
|
||||
(dolist (left '(16.5 0))
|
||||
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||
(lambda (&rest _) (ert-fail "Complete layer rerendered root"))))
|
||||
(ebox-region-update 'card :left `(px ,left)))
|
||||
(should (plist-get (ebox--buffer-render-state (current-buffer))
|
||||
:fractional-pixel-output-p))
|
||||
(ebox-css-layout-test--assert-pixel-lines (buffer-string) 101)
|
||||
(ebox-css-layout-test--assert-fresh-properties))))))
|
||||
|
||||
(ert-deftest ebox-css-layout-fractional-layer-rolls-back-normalized-output ()
|
||||
"Failed mixed publication restores raw provenance and retries local layers."
|
||||
(ebox-css-layout-test--with-pixel-display
|
||||
(let ((ebox-viewport-width 101) (ebox-viewport-height 8))
|
||||
(with-temp-buffer
|
||||
(ebox-render-to-buffer
|
||||
(current-buffer) (ebox-css-layout-test--fractional-layer nil '(px 0)))
|
||||
(let ((state (ebox--buffer-render-state (current-buffer)))
|
||||
(revision (ebox-surface-buffer-revision (current-buffer)))
|
||||
(contents (buffer-string)))
|
||||
(should-not (plist-get state :fractional-pixel-output-p))
|
||||
(cl-labels ((update ()
|
||||
(ebox-call-with-update-batch
|
||||
(lambda ()
|
||||
(ebox-region-update 'card :left '(px 16.5))
|
||||
(ebox-region-update 'status :content "Moved")))))
|
||||
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||
(lambda (&rest _) (ert-fail "Layer retry rerendered root"))))
|
||||
(dolist (failure-step '(text client-state))
|
||||
(let* ((failed-step nil)
|
||||
(tp--surface-publication-step-function
|
||||
(lambda (step _surface)
|
||||
(when (eq step failure-step)
|
||||
(setq failed-step step)
|
||||
(error "Reject normalized layer %S" step)))))
|
||||
(should-error (update))
|
||||
(should (eq failed-step failure-step)))
|
||||
(should (= (ebox-surface-buffer-revision (current-buffer)) revision))
|
||||
(should (eq (ebox--buffer-render-state (current-buffer)) state))
|
||||
(should-not (plist-get state :fractional-pixel-output-p))
|
||||
(should (equal-including-properties contents (buffer-string))))
|
||||
(update)))
|
||||
(should (= (ebox-surface-buffer-revision (current-buffer)) (1+ revision)))
|
||||
(should (plist-get (ebox--buffer-render-state (current-buffer))
|
||||
:fractional-pixel-output-p))
|
||||
(ebox-css-layout-test--assert-pixel-lines (buffer-string) 101)
|
||||
(ebox-css-layout-test--assert-fresh-properties))))))
|
||||
|
||||
(ert-deftest ebox-css-layout-fractional-layer-move-and-paint-preserve-interactions ()
|
||||
"Normalized movement and later paint keep visible interaction properties."
|
||||
(ebox-css-layout-test--with-pixel-display
|
||||
(let ((ebox-viewport-width 101) (ebox-viewport-height 8))
|
||||
(with-temp-buffer
|
||||
(ebox-render-to-buffer
|
||||
(current-buffer) (ebox-css-layout-test--fractional-layer))
|
||||
(dolist (update '((:left (px 16.5)) (:background-color "#556677")))
|
||||
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||
(lambda (&rest _) (ert-fail "Layer paint rerendered root"))))
|
||||
(apply #'ebox-region-update 'card update))
|
||||
(should (plist-get (ebox--buffer-render-state (current-buffer))
|
||||
:fractional-pixel-output-p))
|
||||
(ebox-css-layout-test--assert-pixel-lines (buffer-string) 101)
|
||||
(ebox-css-layout-test--assert-fresh-properties)
|
||||
(goto-char (point-min))
|
||||
(search-forward "CARD")
|
||||
(should (equal (get-text-property (1- (point)) 'help-echo) "Moving card"))
|
||||
(should (eq (get-text-property (1- (point)) 'pointer) 'hand))
|
||||
(should (eq (lookup-key (get-text-property (1- (point)) 'keymap) (kbd "RET"))
|
||||
#'ignore)))))))
|
||||
|
||||
(defun ebox-css-layout-test--fractional-scroll (offset &optional boundary mixed)
|
||||
"Build a centered percentage scroll at OFFSET with optional BOUNDARY.
|
||||
MIXED starts with three integral rows before the fractional cached rows."
|
||||
|
||||
@ -138,6 +138,87 @@
|
||||
(should (= full-renders 0))
|
||||
(ebox-layer-publication-test--assert-fresh-render))))
|
||||
|
||||
(ert-deftest ebox-layer-publication-drag-recomposes-only-damaged-rows ()
|
||||
"Moving an overlay recomposes its old and new rows and reuses the rest.
|
||||
|
||||
This is the focused regression for retained composition during a drag: the
|
||||
overlay moves from row one to row two while three underlying rows stay
|
||||
unchanged. Count calls to `ebox-composite-line' to prove that only the two
|
||||
damaged rows are recomposed, then compare the result with a cold composition
|
||||
including all text properties."
|
||||
(let* ((base-0 (list :node-id "base-0" :ebox-kind 'box))
|
||||
(base-1 (list :node-id "base-1" :ebox-kind 'box))
|
||||
(base-2 (list :node-id "base-2" :ebox-kind 'box))
|
||||
(overlay (list :node-id "overlay" :ebox-kind 'box
|
||||
:position 'absolute :z-index 1))
|
||||
(host (list :node-id "drag-host" :ebox-kind 'box
|
||||
:children (list base-0 base-1 base-2 overlay)))
|
||||
(face-base '(foreground-color "white"))
|
||||
(face-overlay '(foreground-color "red" :weight bold))
|
||||
(base-records
|
||||
(list (list :node base-0 :x 0 :y 0
|
||||
:text (propertize "BASE-0" 'face face-base
|
||||
'help-echo "base-0"))
|
||||
(list :node base-1 :x 0 :y 1
|
||||
:text (propertize "BASE-1" 'face face-base
|
||||
'help-echo "base-1"))
|
||||
(list :node base-2 :x 0 :y 2
|
||||
:text (propertize "BASE-2" 'face face-base
|
||||
'help-echo "base-2"))))
|
||||
(first-records
|
||||
(append base-records
|
||||
(list (list :node overlay :x 2 :y 1
|
||||
:text (propertize "DRAG" 'face face-overlay
|
||||
'keymap '(keymap (13 . ignore)))))))
|
||||
(moved-records
|
||||
(append base-records
|
||||
(list (list :node overlay :x 2 :y 2
|
||||
:text (propertize "DRAG" 'face face-overlay
|
||||
'keymap '(keymap (13 . ignore)))))))
|
||||
(context '(drag-test 12 3))
|
||||
(ebox-layer--composition-context context)
|
||||
(first-cache (make-hash-table :test #'equal))
|
||||
(moved-cache (make-hash-table :test #'equal))
|
||||
(first-calls 0)
|
||||
(moved-calls 0)
|
||||
(first-lines nil)
|
||||
(moved-lines nil)
|
||||
first moved cold)
|
||||
;; Cold render seeds the retained ledger for the initial drag position.
|
||||
(let ((ebox-layer--previous-composition-cache nil)
|
||||
(ebox-layer--composition-cache first-cache)
|
||||
(original (symbol-function 'ebox-composite-line)))
|
||||
(cl-letf (((symbol-function 'ebox-composite-line)
|
||||
(lambda (&rest args)
|
||||
(cl-incf first-calls)
|
||||
(push (plist-get (car (cadr args)) :y) first-lines)
|
||||
(funcall original (car args) (cadr args) (caddr args)))))
|
||||
(setq first (ebox-layer--compose-rows host first-records 12 3))))
|
||||
;; The moved candidate reuses row zero and recomposes exactly old/new rows.
|
||||
(let ((ebox-layer--previous-composition-cache first-cache)
|
||||
(ebox-layer--composition-cache moved-cache)
|
||||
(original (symbol-function 'ebox-composite-line)))
|
||||
(cl-letf (((symbol-function 'ebox-composite-line)
|
||||
(lambda (&rest args)
|
||||
(cl-incf moved-calls)
|
||||
(push (plist-get (car (cadr args)) :y) moved-lines)
|
||||
(funcall original (car args) (cadr args) (caddr args)))))
|
||||
(setq moved (ebox-layer--compose-rows host moved-records 12 3))))
|
||||
(should (= first-calls 3))
|
||||
(should (= moved-calls 2))
|
||||
(should (equal (nreverse first-lines) '(0 1 2)))
|
||||
(should (equal (sort moved-lines #'<) '(1 2)))
|
||||
;; A cold render at the moved position is the oracle for text properties.
|
||||
(let ((ebox-layer--previous-composition-cache nil)
|
||||
(ebox-layer--composition-cache nil))
|
||||
(setq cold (ebox-layer--compose-rows host moved-records 12 3)))
|
||||
(should (equal-including-properties moved cold))
|
||||
;; The retained row is the same object, proving ledger reuse rather than
|
||||
;; merely an equivalent recomposition.
|
||||
(should (eq (nth 0 moved) (nth 0 first)))
|
||||
(should-not (eq (nth 1 moved) (nth 1 first)))
|
||||
(should-not (eq (nth 2 moved) (nth 2 first)))))
|
||||
|
||||
(ert-deftest ebox-layer-publication-rejected-hidden-commit-rolls-back ()
|
||||
"Rejected hidden updates leave retained input, revision and text unchanged."
|
||||
(ebox-layer-publication-test--with-buffer
|
||||
|
||||
Loading…
Reference in New Issue
Block a user