Fix retained Flex ownership and responsive viewport updates
This commit is contained in:
parent
33c625ad82
commit
b65b5937cc
@ -25,6 +25,7 @@
|
||||
"ebox" (lines))
|
||||
(declare-function ebox-string-lines
|
||||
"ebox" (string))
|
||||
(defvar ebox-region-types)
|
||||
|
||||
(defvar ebox-render-gc-cons-threshold (* 128 1024 1024)
|
||||
"Temporary `gc-cons-threshold' used during Ebox render transactions.
|
||||
@ -501,6 +502,81 @@ may be added in place."
|
||||
|
||||
;;; Existing Slot Shaping
|
||||
|
||||
(defun ebox-buffer--rendered-owned-lines
|
||||
(rendered region-ids &optional expected-line-count)
|
||||
"Project RENDERED to its contiguous direct role-owned lines.
|
||||
|
||||
REGION-IDS is the same owner set used to derive retained TP spans. Leading
|
||||
and trailing lines without a direct target role are stable wrapper chrome and
|
||||
are omitted. When EXPECTED-LINE-COUNT is non-nil, any remaining excess may
|
||||
be removed only from role-proven outer margin/border lines (`mt'/`bt' at the
|
||||
start, `mb'/`bb' at the end). Padding and content are never inferred from a
|
||||
numeric box-model delta. An unowned interior line or a foreign direct
|
||||
`content' role fails closed. Returned lines retain all text properties."
|
||||
(let ((region-set (make-hash-table :test 'equal))
|
||||
(lines (ebox-string-lines rendered))
|
||||
infos first last invalid)
|
||||
(dolist (region-id region-ids)
|
||||
(puthash region-id t region-set))
|
||||
(dolist (line lines)
|
||||
(let ((position 0)
|
||||
(limit (length line))
|
||||
owned foreign-content roles)
|
||||
(while (< position limit)
|
||||
(let ((next limit))
|
||||
(dolist (entry ebox-region-types)
|
||||
(let* ((property (cdr entry))
|
||||
(region-id (get-text-property position property line))
|
||||
(change (next-single-property-change
|
||||
position property line limit)))
|
||||
(when (and region-id (gethash region-id region-set))
|
||||
(setq owned t)
|
||||
(cl-pushnew (car entry) roles))
|
||||
(when (and (eq (car entry) 'content) region-id
|
||||
(not (gethash region-id region-set)))
|
||||
(setq foreign-content t))
|
||||
(when (and change (< change next))
|
||||
(setq next change))))
|
||||
(setq position (max next (1+ position)))))
|
||||
(push (list :line line :owned (and owned (not foreign-content))
|
||||
:roles roles)
|
||||
infos)
|
||||
(when (and owned foreign-content)
|
||||
(setq invalid t))))
|
||||
(setq infos (nreverse infos))
|
||||
(cl-loop for info in infos
|
||||
for index from 0
|
||||
when (plist-get info :owned)
|
||||
do (unless first (setq first index))
|
||||
and do (setq last index))
|
||||
(when (and first (not invalid)
|
||||
(cl-every (lambda (info) (plist-get info :owned))
|
||||
(cl-subseq infos first (1+ last))))
|
||||
(let ((owned (cl-subseq infos first (1+ last))))
|
||||
(when expected-line-count
|
||||
(while (> (length owned) expected-line-count)
|
||||
(let ((leading-roles (plist-get (car owned) :roles))
|
||||
(trailing-roles (plist-get (car (last owned)) :roles)))
|
||||
(cond
|
||||
((and (not (memq 'content leading-roles))
|
||||
(not (memq 'content-owner leading-roles))
|
||||
(cl-some (lambda (role) (memq role '(mt bt)))
|
||||
leading-roles))
|
||||
(setq owned (cdr owned)))
|
||||
((and (not (memq 'content trailing-roles))
|
||||
(not (memq 'content-owner trailing-roles))
|
||||
(cl-some (lambda (role) (memq role '(mb bb)))
|
||||
trailing-roles))
|
||||
(setq owned (butlast owned)))
|
||||
(t
|
||||
(setq invalid t
|
||||
owned nil))))))
|
||||
(when (and owned (not invalid)
|
||||
(or (null expected-line-count)
|
||||
(= (length owned) expected-line-count)))
|
||||
(ebox-lines-join
|
||||
(mapcar (lambda (info) (plist-get info :line)) owned)))))))
|
||||
|
||||
(defun ebox-buffer--span-slot-width (span)
|
||||
"Return SPAN's visual slot width in its current buffer line."
|
||||
(save-excursion
|
||||
|
||||
14
ebox-flex.el
14
ebox-flex.el
@ -390,8 +390,15 @@ grapheme."
|
||||
box (ebox-get box :min-width) 0)
|
||||
0)))
|
||||
(if (null (ebox-get box :wrap-mode))
|
||||
;; CSS's automatic minimum is min-content only when the author
|
||||
;; leaves `min-width' at its automatic value. A component that
|
||||
;; deliberately declares `:min-width 0' must be allowed to
|
||||
;; shrink even when its own rows remain no-wrap (the surrounding
|
||||
;; card may then own horizontal overflow).
|
||||
(if (ebox--flex-explicit-min-main-p box axis)
|
||||
(+ (ebox--side-pixel box) declared-min)
|
||||
(max (ebox--flex-string-main rendered axis)
|
||||
(+ (ebox--side-pixel box) declared-min))
|
||||
(+ (ebox--side-pixel box) declared-min)))
|
||||
(let ((content-min (ebox--content-min-pixel box)))
|
||||
(when (hash-table-p ebox--flex-content-min-width-table)
|
||||
(puthash box content-min
|
||||
@ -404,6 +411,11 @@ grapheme."
|
||||
box (ebox-get box :min-height))
|
||||
0)))))
|
||||
|
||||
(defun ebox--flex-explicit-min-main-p (box axis)
|
||||
"Return non-nil when BOX declares a main-axis minimum explicitly."
|
||||
(let ((property (if (eq axis 'row) 'ebox/min-width 'ebox/min-height)))
|
||||
(plist-member (plist-get box :ebox-style-declarations) property)))
|
||||
|
||||
(defun ebox--flex-box-main-constraint (box axis property)
|
||||
"Return BOX's total main-axis size constraint from PROPERTY."
|
||||
(when (plist-member box property)
|
||||
|
||||
@ -48,6 +48,7 @@ are not part of its final allocated geometry."
|
||||
:viewport-height-independent
|
||||
(and (boundp 'ebox-viewport-height) ebox-viewport-height))))
|
||||
(list (ebox--ensure-node-id source)
|
||||
(copy-sequence (ebox--node-all-region-ids source))
|
||||
'flex-final-sized
|
||||
axis main cross align viewport
|
||||
height-key
|
||||
|
||||
@ -2911,6 +2911,36 @@ its new minimum must still fit CURRENT-MAIN."
|
||||
(<= (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.
|
||||
@ -2918,19 +2948,29 @@ 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 (not (cl-some #'ebox--flex-content-flow-main-key-p
|
||||
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 (ebox--flex-item-props child)
|
||||
axis current-main)))))
|
||||
buffer style-node item-props axis current-main))))))
|
||||
|
||||
(defun ebox--flex-item-slot-sized-node
|
||||
(buffer node-id snapshot &optional changed-keys)
|
||||
@ -3006,19 +3046,36 @@ CHANGED-KEYS must prove that reusing the allocation is safe."
|
||||
(or (ebox--render-node-in-current-flex-slot
|
||||
buffer node-id snapshot changed-keys)
|
||||
(ebox-render 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 rendered)))
|
||||
(final-rendered (or slot-rendered rendered))
|
||||
(ebox-buffer--rendered-in-existing-slots
|
||||
spans owned-rendered
|
||||
(and role-owned-lines-p owner-set)))))
|
||||
(final-rendered slot-rendered)
|
||||
(new-span-footprint
|
||||
(ebox--rendered-span-footprint-signature final-rendered))
|
||||
(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-span-footprint)))
|
||||
(new-parent-slot
|
||||
(and new-span-footprint
|
||||
(ebox--project-parent-slot-signature
|
||||
old-parent-slot new-span-footprint)))
|
||||
(and (ebox--span-footprint-compatible-p
|
||||
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)
|
||||
@ -3058,7 +3115,9 @@ grow/shrink) legitimately changes with it, so a slot-preserving
|
||||
;; 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-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)))
|
||||
|
||||
@ -4634,6 +4693,14 @@ DETACHED-HISTORY receives identity snapshots for semantic replacements."
|
||||
(aref old-payload offset)))))
|
||||
(when (and old key (null (plist-get old :key))) (setq old nil))
|
||||
(when (and old (null key) (plist-get 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 (plist-get old :key)
|
||||
(not (equal (plist-get old :key) key)))
|
||||
(setq old nil))
|
||||
(ebox-tree-reconcile-runtime old item)))
|
||||
items))
|
||||
|
||||
@ -4694,14 +4761,14 @@ DETACHED-HISTORY receives identity snapshots for semantic replacements."
|
||||
table)))))
|
||||
|
||||
(defun ebox-incremental--candidate-path-copy-overlay
|
||||
(root node-id replacement state)
|
||||
(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 t)
|
||||
node-id replacement parent-id t structure-p)
|
||||
(while parent-id
|
||||
(let* ((current-parent-id parent-id)
|
||||
(old-parent
|
||||
@ -4721,7 +4788,7 @@ DETACHED-HISTORY receives identity snapshots for semantic 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 t)
|
||||
current-parent-id new-parent grandparent-id nil structure-p)
|
||||
(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)))
|
||||
@ -4808,17 +4875,30 @@ DETACHED-HISTORY receives identity snapshots for semantic replacements."
|
||||
(ebox-child-range--lookup-ref
|
||||
(car result) (car replacement))) nil))
|
||||
ordered-replacements))
|
||||
(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)))
|
||||
(plist-put new-parent :ebox-child-sequence (car result))
|
||||
(push (ebox-child-range--metrics-plist (cdr result)) metrics)
|
||||
(setq root
|
||||
(ebox-incremental--candidate-path-copy-overlay
|
||||
root parent-id new-parent state))
|
||||
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-payloads
|
||||
:new-payloads new-payloads)
|
||||
ebox-incremental--candidate-range-index-deltas))))))))
|
||||
@ -5297,7 +5377,8 @@ complete preorder first-owner and postorder last-box semantics."
|
||||
entry))
|
||||
(when old-node
|
||||
(let* ((changed-keys
|
||||
(ebox-tree-node-local-changed-keys old-node new-node))
|
||||
(ebox-incremental--candidate-local-changed-keys
|
||||
old-node new-node))
|
||||
(range-delta
|
||||
(and range-delta-table
|
||||
(gethash (plist-get new-node :node-id)
|
||||
@ -5437,7 +5518,7 @@ replace those O(n) table copies without changing this delta contract."
|
||||
(record-touched old-parent new-parent
|
||||
(gethash parent-id
|
||||
(plist-get old-state :parent-table))
|
||||
t)
|
||||
(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))
|
||||
@ -5722,6 +5803,23 @@ recognized from its first untouched sibling without walking the whole tree."
|
||||
(visit candidate-root)
|
||||
nil)))))
|
||||
|
||||
(defun ebox-incremental--candidate-local-changed-keys (old new)
|
||||
"Return real source changes between published OLD and candidate NEW.
|
||||
|
||||
Published nodes carry computed ECSS longhands while fresh candidate nodes are
|
||||
still declaration-stage values. When canonical declarations are unchanged,
|
||||
discard style longhand and container-prop differences caused solely by that
|
||||
stage mismatch; selector metadata and non-style source fields remain visible."
|
||||
(let ((changed (ebox-tree-node-local-changed-keys old new)))
|
||||
(if (equal (ebox-style-node-declarations old)
|
||||
(ebox-style-node-declarations new))
|
||||
(cl-remove-if
|
||||
(lambda (key)
|
||||
(or (eq key :props)
|
||||
(ebox-style-property key)))
|
||||
changed)
|
||||
changed)))
|
||||
|
||||
(defun ebox-incremental--declarative-dirty-set
|
||||
(old-state candidate-root candidate-index)
|
||||
"Return source-level dirty entries from OLD-STATE to CANDIDATE-ROOT.
|
||||
@ -6386,9 +6484,18 @@ 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) (eq (plist-get entry :dirty-kind) 'geometry))
|
||||
dirty-set))
|
||||
dirty-set)))
|
||||
(paint-dirty
|
||||
(cl-remove-if-not
|
||||
(lambda (entry) (eq (plist-get entry :dirty-kind) 'paint))
|
||||
@ -6425,14 +6532,27 @@ suppresses the ordinary fallback when any fact is missing."
|
||||
(geometry-span-proof
|
||||
(copy-sequence (plist-get geometry-span-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--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 paint-dirty)))
|
||||
(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 paint-dirty
|
||||
(and external-paint-dirty
|
||||
(cl-every
|
||||
(lambda (entry)
|
||||
(and (plist-get entry :node-id)
|
||||
@ -6469,7 +6589,7 @@ suppresses the ordinary fallback when any fact is missing."
|
||||
(plist-get
|
||||
(plist-get entry :new-signature)
|
||||
:content))))))
|
||||
paint-dirty)))
|
||||
external-paint-dirty)))
|
||||
(disjoint-p
|
||||
(and geometry-owner-ids
|
||||
(not (cl-some (lambda (geometry-id)
|
||||
@ -6598,6 +6718,54 @@ suppresses the ordinary fallback when any fact is missing."
|
||||
'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 local Flex-item op for related geometry RENDER-DIRTY-SET.
|
||||
|
||||
Multiple descendant content changes can share a fixed-basis, zero-minimum Flex
|
||||
item without reflowing the outer Flex line. The candidate item is still
|
||||
rendered into its retained slot and must pass the complete footprint proof."
|
||||
(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)))
|
||||
(changed-keys
|
||||
(delete-dups
|
||||
(cl-mapcan
|
||||
(lambda (entry)
|
||||
(copy-sequence (plist-get entry :changed-keys)))
|
||||
render-dirty-set))))
|
||||
(when (and owner-id
|
||||
(not (equal owner-id (ebox--buffer-root-node-id buffer)))
|
||||
(ebox--cached-flex-item-slot-footprint-safe-p
|
||||
buffer owner-id changed-keys))
|
||||
(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."
|
||||
@ -6624,6 +6792,8 @@ suppresses the ordinary fallback when any fact is missing."
|
||||
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))
|
||||
@ -7603,13 +7773,120 @@ metadata; this predicate only authorizes the local attempt."
|
||||
(setq valid nil)))))
|
||||
valid))
|
||||
|
||||
(defun ebox-incremental--fixed-basis-flex-group-span-proof
|
||||
(buffer old-state prepared candidate-state owner-plan)
|
||||
"Return a single-owner proof for grouped fixed-basis Flex descendants.
|
||||
|
||||
The layout owner plan has already established one content-independent Flex
|
||||
allocation. This final gate renders the complete candidate item into its
|
||||
published slot, verifies exact footprint compatibility, and packages the
|
||||
result in the same owner-proof shape consumed by span 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)))
|
||||
(snapshot
|
||||
(and old-node
|
||||
(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)))
|
||||
(allocated-width
|
||||
(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-surface--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))
|
||||
(eq (plist-get operation :op) '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)))
|
||||
(plist-get entry :changed-keys))))
|
||||
dirty-set)
|
||||
(ebox--flex-item-fixed-basis-content-allocation-stable-p
|
||||
buffer owner-id)
|
||||
(ebox--cached-flex-item-slot-footprint-safe-p
|
||||
buffer owner-id changed-keys)
|
||||
(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
|
||||
(plist-get old-state :root-node)
|
||||
(plist-get candidate-state :root-node))
|
||||
(null (plist-get old-state :scroll-region-ids))
|
||||
(not (ebox--node-visible-overflow-p old-node))
|
||||
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
|
||||
:role-owned-lines-p t
|
||||
:ancestor-slot-signature
|
||||
(list (list :node-id owner-id
|
||||
:parent-slot-signature
|
||||
(plist-get snapshot
|
||||
:parent-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--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)
|
||||
(ebox-incremental--single-span-patch-projection-proof
|
||||
(or (ebox-incremental--single-span-patch-projection-proof
|
||||
buffer old-state prepared candidate-state owner-plan path-copied-p)
|
||||
(ebox-incremental--fixed-basis-flex-group-span-proof
|
||||
buffer old-state prepared candidate-state owner-plan))
|
||||
(let (proofs valid)
|
||||
(setq valid (and owner-plan
|
||||
(= (length owner-plan)
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
(defvar ebox-viewport-height)
|
||||
(defvar ebox--render-display-signature)
|
||||
(defvar ebox-incremental--allocated-slot-proof-cache)
|
||||
(defvar ebox-incremental--buffer-render-state-override)
|
||||
|
||||
(declare-function ebox--render-layout "ebox-layout" (node))
|
||||
(declare-function ebox--record-render-output-provenance
|
||||
@ -57,6 +58,13 @@
|
||||
"ebox-incremental" (target keys source))
|
||||
(declare-function ebox--runtime-region-id-conflict
|
||||
"ebox-incremental" (region-id-set target-buffer))
|
||||
(declare-function ebox--region-id-set
|
||||
"ebox-incremental" (region-ids))
|
||||
(declare-function ebox--flex-item-slot-sized-node
|
||||
"ebox-incremental"
|
||||
(buffer node-id snapshot &optional changed-keys))
|
||||
(declare-function ebox--layout-snapshot-strip-details
|
||||
"ebox-incremental" (snapshot))
|
||||
(declare-function ebox--viewport-dependent-node-id-axes
|
||||
"ebox-incremental" (node))
|
||||
(declare-function ebox--update-report
|
||||
@ -575,6 +583,19 @@ FRAMEWORK-PARTICIPANT owns paired framework publication when non-nil."
|
||||
(plist-put new-state :surface surface)
|
||||
(plist-put new-state :runtime-revision
|
||||
(1- (tp-surface-revision surface)))
|
||||
;; A fixed-basis role-owned patch rendered against the previous
|
||||
;; allocation. Its installed details describe that proof render,
|
||||
;; not the now-committed TP mounts. Drop only those owners' derived
|
||||
;; geometry so the next transaction lazily captures the committed
|
||||
;; spans and cannot drift across a repeated selection round trip.
|
||||
(when-let ((snapshots (plist-get new-state :layout-snapshots)))
|
||||
(dolist (proof (plist-get new-state :owner-scoped-proofs))
|
||||
(when (plist-get proof :role-owned-lines-p)
|
||||
(let ((owner-id (plist-get proof :owner-id)))
|
||||
(when-let ((snapshot (gethash owner-id snapshots)))
|
||||
(puthash owner-id
|
||||
(ebox--layout-snapshot-strip-details snapshot)
|
||||
snapshots))))))
|
||||
(when (plist-get report-base :scroll-state-transaction)
|
||||
;; Scroll changes invalidate every buffer-span coordinate. Keep
|
||||
;; the committed generation structurally snapshot-free so a later
|
||||
@ -1615,9 +1636,9 @@ valid because their declarative layout and viewport did not change."
|
||||
(string< (symbol-name left)
|
||||
(symbol-name right))))))))
|
||||
|
||||
(defun ebox-surface--span-patch-lines (buffer spans rendered
|
||||
&optional variable-content-p
|
||||
variable-content-max-width)
|
||||
(defun ebox-surface--span-patch-lines
|
||||
(buffer spans rendered &optional variable-content-p
|
||||
variable-content-max-width owner-set)
|
||||
"Return RENDERED shaped for BUFFER SPANS, or nil when unsafe.
|
||||
When VARIABLE-CONTENT-P is non-nil, allow a stable partial slot to retain its
|
||||
old allocated width while the owner changes its natural content length."
|
||||
@ -1633,7 +1654,8 @@ old allocated width while the owner changes its natural content length."
|
||||
variable-content-max-width))
|
||||
lines)))
|
||||
rendered))
|
||||
(ebox-buffer--rendered-in-existing-slots spans rendered nil t))
|
||||
(ebox-buffer--rendered-in-existing-slots
|
||||
spans rendered owner-set t))
|
||||
(and (= (length spans) (length (ebox-string-lines rendered)))
|
||||
rendered))))
|
||||
|
||||
@ -1756,7 +1778,8 @@ old allocated width while the owner changes its natural content length."
|
||||
|
||||
(defun ebox-surface--owner-patch-candidate
|
||||
(buffer state owner-id allocated-width
|
||||
&optional variable-content-p variable-content-max-width)
|
||||
&optional variable-content-p variable-content-max-width
|
||||
role-owned-lines-p changed-keys)
|
||||
"Return validated patch details for OWNER-ID, or nil."
|
||||
(let* (
|
||||
(old-snapshot
|
||||
@ -1764,16 +1787,36 @@ old allocated width while the owner changes its natural content length."
|
||||
(spans (and old-snapshot (plist-get old-snapshot :buffer-spans)))
|
||||
(node (and owner-id (gethash owner-id (plist-get state :node-table))))
|
||||
(render-node
|
||||
(if role-owned-lines-p
|
||||
;; The planner proved this fixed-basis owner in its published
|
||||
;; Flex slot. Recreate that exact candidate-state witness for
|
||||
;; publication; a natural render can have fewer owned lines and
|
||||
;; would spuriously widen the TP scope to the root.
|
||||
(and (fboundp 'ebox--flex-item-slot-sized-node)
|
||||
(let ((ebox-incremental--buffer-render-state-override
|
||||
(cons buffer state)))
|
||||
(ebox--flex-item-slot-sized-node
|
||||
buffer owner-id old-snapshot changed-keys)))
|
||||
(if allocated-width
|
||||
(plist-put (copy-sequence node) :width allocated-width)
|
||||
node)))
|
||||
(when (and old-snapshot spans node)
|
||||
node))))
|
||||
(when (and old-snapshot spans node render-node)
|
||||
(let* ((rendered
|
||||
(ebox-surface--render-candidate-node state render-node))
|
||||
(owned-rendered
|
||||
(if role-owned-lines-p
|
||||
(ebox-buffer--rendered-owned-lines
|
||||
rendered (plist-get old-snapshot :region-ids)
|
||||
(length spans))
|
||||
rendered))
|
||||
(replacement
|
||||
(and owned-rendered
|
||||
(ebox-surface--span-patch-lines
|
||||
buffer spans rendered variable-content-p
|
||||
variable-content-max-width))
|
||||
buffer spans owned-rendered variable-content-p
|
||||
variable-content-max-width
|
||||
(and role-owned-lines-p
|
||||
(ebox--region-id-set
|
||||
(plist-get old-snapshot :region-ids))))))
|
||||
(details
|
||||
(and replacement
|
||||
(ebox-surface--span-patch-details
|
||||
@ -1794,7 +1837,9 @@ old allocated width while the owner changes its natural content length."
|
||||
buffer state (plist-get proof :owner-id)
|
||||
(plist-get proof :allocated-width)
|
||||
(plist-get proof :variable-content-p)
|
||||
(plist-get proof :variable-content-max-width)))
|
||||
(plist-get proof :variable-content-max-width)
|
||||
(plist-get proof :role-owned-lines-p)
|
||||
(plist-get proof :changed-keys)))
|
||||
proofs)
|
||||
(list
|
||||
(ebox-surface--owner-patch-candidate
|
||||
|
||||
36
ebox.el
36
ebox.el
@ -2743,6 +2743,42 @@ that was rendered off-window cannot miss the native-scroll handoff."
|
||||
(when (boundp 'window-state-change-functions)
|
||||
(add-hook 'window-state-change-functions #'ebox--window-state-change))
|
||||
|
||||
(defvar ebox--window-size-change-in-progress nil
|
||||
"Non-nil while visible Ebox buffers are synchronizing their viewport.")
|
||||
|
||||
(defun ebox--window-size-change (frame)
|
||||
"Synchronize visible Ebox buffers after FRAME changes size.
|
||||
|
||||
Ebox stores the containing-block viewport in its mounted surface. A frame
|
||||
resize must publish the new window content width before the next render;
|
||||
otherwise responsive Flex/Grid trees keep using the width from mount time.
|
||||
One mounted surface represents one viewport, so when a buffer is visible in
|
||||
multiple windows the first window in FRAME supplies the shared dimensions."
|
||||
(when (and (frame-live-p frame)
|
||||
(not noninteractive)
|
||||
(not ebox--window-size-change-in-progress))
|
||||
(let ((ebox--window-size-change-in-progress t)
|
||||
(seen (make-hash-table :test #'eq)))
|
||||
(dolist (window (window-list frame 'no-minibuf))
|
||||
(when (and (window-live-p window)
|
||||
(not (gethash (window-buffer window) seen)))
|
||||
(let ((buffer (window-buffer window)))
|
||||
(puthash buffer t seen)
|
||||
(when (and (buffer-live-p buffer)
|
||||
(ebox--buffer-render-state buffer)
|
||||
(ebox-surface-buffer-mounted-p buffer))
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(width (ebox-surface--window-content-width window))
|
||||
(height (window-body-height window)))
|
||||
(unless (and (= width (or (plist-get state :viewport-width)
|
||||
-1))
|
||||
(= height (or (plist-get state :viewport-height)
|
||||
-1)))
|
||||
(ebox-rerender-buffer-with-context buffer width height))))))))))
|
||||
|
||||
(when (boundp 'window-size-change-functions)
|
||||
(add-hook 'window-size-change-functions #'ebox--window-size-change))
|
||||
|
||||
(defun ebox--runtime-prewarm-record-viewport-node
|
||||
(job node width-allowed)
|
||||
"Record NODE's direct viewport dependencies in JOB.
|
||||
|
||||
@ -481,6 +481,109 @@
|
||||
:range-ref-table))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-child-range-stable-keyed-refresh-is-not-structural ()
|
||||
"Stable keyed Range refreshes must not become root structure updates."
|
||||
(let ((buffer (generate-new-buffer " *ebox-stable-range-dirty*")))
|
||||
(cl-labels
|
||||
((rows (selected)
|
||||
(cl-loop for id from 1 to 12
|
||||
collect
|
||||
(ebox-create
|
||||
:key id :content (format "row-%02d" id) :width '(420)
|
||||
:color (and (= id selected) "#2F6B43"))))
|
||||
(root ()
|
||||
(ebox-create
|
||||
:width '(900) :color "#172033" :wrap-mode nil
|
||||
:ebox-content-node
|
||||
(ebox-flex
|
||||
:width '(900)
|
||||
(ebox-create
|
||||
:key 'list :width 'stretch :min-width 0 :wrap-mode nil
|
||||
:min-height 24 :padding '(2 2) :border "#687386"
|
||||
:flex-grow 2 :flex-shrink 1 :flex-basis '(600)
|
||||
:ebox-content-node
|
||||
(ebox-column
|
||||
(apply #'ebox-child-range 'items (rows 1))))
|
||||
(ebox-create :key 'detail :host-ref 'detail
|
||||
:width 'stretch :min-width 0
|
||||
:min-height 24 :padding '(2 2) :border "#687386"
|
||||
:flex-grow 1 :flex-shrink 1 :flex-basis '(300)
|
||||
:content "detail-1")))))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-render-to-buffer buffer (root))
|
||||
(let ((root-id (ebox--buffer-root-node-id buffer)))
|
||||
(dolist (selected '(2 1 2))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items (rows selected))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'detail
|
||||
(ebox-create :key 'detail :host-ref 'detail
|
||||
:width 'stretch :min-width 0
|
||||
:min-height 24 :padding '(2 2)
|
||||
:border "#687386"
|
||||
:flex-grow 1 :flex-shrink 1 :flex-basis '(300)
|
||||
:content (format "detail-%d" selected)))
|
||||
(let ((report (ebox-commit buffer candidate)))
|
||||
(ert-info ((format "selected=%S report=%S"
|
||||
selected report))
|
||||
(should-not (memq 'structure
|
||||
(plist-get report :dirty-kinds)))
|
||||
(should-not (member root-id
|
||||
(plist-get report :owner-ids)))
|
||||
(should (memq (plist-get report :strategy)
|
||||
'(span-patch owner-rerender
|
||||
mixed-owner-reflow)))
|
||||
(should (= (plist-get report :tp-scope-count) 3))
|
||||
(should (= (plist-get report :created-objects) 0))
|
||||
(should (= (plist-get report :removed-objects) 0))))))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer))))))
|
||||
|
||||
(ert-deftest ebox-child-range-does-not-reuse-disappeared-key-positionally ()
|
||||
"A new keyed item must not inherit a removed peer's runtime identity."
|
||||
(let ((buffer (generate-new-buffer " *ebox-range-key-reentry*")))
|
||||
(cl-labels
|
||||
((rows (keys)
|
||||
(mapcar (lambda (key)
|
||||
(ebox-create :key key
|
||||
:content (format "row-%s" key)))
|
||||
keys))
|
||||
(row-id (key)
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(resolved (ebox-incremental--range-ref-resolve
|
||||
state 'items))
|
||||
(segment (ebox-child-range--segment-at
|
||||
(plist-get resolved :sequence)
|
||||
(plist-get resolved :segment-index)))
|
||||
(node (cl-find key
|
||||
(append
|
||||
(ebox-child-range--segment-payload segment)
|
||||
nil)
|
||||
:key (lambda (item)
|
||||
(plist-get item :key)))))
|
||||
(plist-get node :node-id))))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column
|
||||
(apply #'ebox-child-range 'items (rows '(1 2 3 4)))))
|
||||
(let ((initial-1 (row-id 1))
|
||||
(initial-4 (row-id 4)))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items (rows '(1 4)))
|
||||
(ebox-commit buffer candidate))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items (rows '(1 2 3 4)))
|
||||
(ebox-commit buffer candidate))
|
||||
(should (= initial-1 (row-id 1)))
|
||||
(should (= initial-4 (row-id 4)))
|
||||
(should-not (= initial-4 (row-id 2)))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer))))))
|
||||
|
||||
(ert-deftest ebox-child-range-sole-child-keeps-public-layout-parent ()
|
||||
"Keep row/column material parents for one empty or populated Range."
|
||||
(dolist (constructor '(ebox-row ebox-column))
|
||||
|
||||
@ -1254,6 +1254,91 @@ remain retained identities."
|
||||
(ebox-create :key 'untouched :host-ref 'untouched
|
||||
:content "untouched")))
|
||||
|
||||
(defun ebox-commit-test--fixed-basis-selection-root (row-1 row-2)
|
||||
"Return a stretched fixed-basis panel containing two selectable rows."
|
||||
(let ((panel
|
||||
(ebox-create
|
||||
:key 'selection-panel :width 'stretch :min-width 0 :min-height 24
|
||||
:flex-grow 2 :flex-shrink 1 :flex-basis '(340)
|
||||
:ebox-content-node
|
||||
(ebox-column
|
||||
(ebox-create :key 'row-1 :host-ref 'row-1 :content row-1)
|
||||
(ebox-create :key 'row-2 :host-ref 'row-2 :content row-2)))))
|
||||
(ebox-flex
|
||||
:key 'fixed-basis-selection-root
|
||||
:width '(900) :height 24
|
||||
:flex-flow '(row nowrap) :align-items 'stretch
|
||||
(ebox-create :key 'peer :content "peer" :width 'stretch
|
||||
:min-width 0 :min-height 24
|
||||
:flex-grow 4 :flex-shrink 1 :flex-basis '(620))
|
||||
panel)))
|
||||
|
||||
(defun ebox-commit-test--fixed-basis-selection-candidate
|
||||
(buffer row-1 row-2)
|
||||
"Return BUFFER candidate replacing both fixed-basis selection rows."
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'row-1
|
||||
(ebox-create :key 'row-1 :host-ref 'row-1 :content row-1))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'row-2
|
||||
(ebox-create :key 'row-2 :host-ref 'row-2 :content row-2))
|
||||
candidate))
|
||||
|
||||
(ert-deftest ebox-commit-fixed-basis-selection-round-trip-stays-local ()
|
||||
"Continuous row-1 -> row-2 -> row-1 publication keeps local TP scope."
|
||||
(let ((buffer
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-fixed-basis-round-trip* ")
|
||||
(ebox-commit-test--fixed-basis-selection-root
|
||||
"[x] row 1" "[ ] row 2"))))
|
||||
(unwind-protect
|
||||
(let* ((root-id (ebox--buffer-root-node-id buffer))
|
||||
(panel-id
|
||||
(plist-get (ebox--host-ref-node buffer 'row-1) :node-id))
|
||||
reports)
|
||||
(setq panel-id
|
||||
(ebox-incremental--nearest-fixed-basis-flex-item-owner-id
|
||||
buffer panel-id))
|
||||
(dolist (contents '(("[ ] row 1" "[x] row 2")
|
||||
("[x] row 1" "[ ] row 2")))
|
||||
(let* ((report
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-commit-test--fixed-basis-selection-candidate
|
||||
buffer (car contents) (cadr contents))))
|
||||
(surface (with-current-buffer
|
||||
buffer ebox-surface--buffer-surface))
|
||||
(tp-report (tp-surface-report surface))
|
||||
(object-count
|
||||
(plist-get (tp-surface-inspect surface) :object-count))
|
||||
(snapshots
|
||||
(plist-get (ebox--buffer-render-state buffer)
|
||||
:layout-snapshots))
|
||||
(panel-snapshot (and snapshots
|
||||
(gethash panel-id snapshots))))
|
||||
(push report reports)
|
||||
(should (eq (plist-get report :projection-kind)
|
||||
'owner-scoped))
|
||||
(should-not (member root-id (plist-get report :owner-ids)))
|
||||
(should-not (plist-get report :tp-full-root))
|
||||
(should-not (plist-get report :tp-scope-fallback))
|
||||
(should (< (plist-get tp-report :reconciled-objects)
|
||||
object-count))
|
||||
(should (<= (plist-get tp-report :reconciled-objects) 4))
|
||||
;; Successful publication leaves the fixed-basis owner ready
|
||||
;; to recapture geometry from the committed TP mounts.
|
||||
(should panel-snapshot)
|
||||
(should-not (plist-member panel-snapshot :buffer-spans))
|
||||
(with-current-buffer buffer
|
||||
(should
|
||||
(equal
|
||||
(buffer-substring-no-properties (point-min) (point-max))
|
||||
(substring-no-properties
|
||||
(ebox-render (ebox--buffer-root-node buffer))))))))
|
||||
(should (= (length reports) 2)))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(defun ebox-commit-test--mixed-owner-candidate
|
||||
(buffer left right paint-a paint-b paint-c)
|
||||
"Return BUFFER candidate replacing all mixed fixture owners."
|
||||
|
||||
@ -6698,6 +6698,61 @@
|
||||
(buffer-substring-no-properties (point-min) (point-max))))
|
||||
(should (ebox-test--buffer-visually-matches-runtime-render-p))))))
|
||||
|
||||
(ert-deftest ebox-flex-grown-fixed-basis-item-content-update-keeps-current-slot ()
|
||||
"A zero-minimum fixed-basis item may update inside its grown slot."
|
||||
(ebox-test--reset-runtime-state)
|
||||
(let ((layout
|
||||
(ebox-build
|
||||
'(flex :id root :width (900) :height 24
|
||||
:flex-flow (row nowrap) :align-items stretch
|
||||
(box :id list :content "peer" :width stretch :min-width 0
|
||||
:min-height 24 :flex-grow 4 :flex-shrink 1
|
||||
:flex-basis (620))
|
||||
(box :id target :content "detail one" :width stretch :min-width 0
|
||||
:min-height 24 :flex-grow 2 :flex-shrink 1
|
||||
:flex-basis (340))))))
|
||||
(ebox-test--with-rendered-buffer layout
|
||||
(let* ((match (car (ebox-selector-query-buffer
|
||||
(current-buffer) "#target")))
|
||||
(region-id (plist-get match :region-id))
|
||||
(node-id (plist-get match :node-id))
|
||||
(root-id (ebox--buffer-root-node-id (current-buffer))))
|
||||
(ebox-test--region-update region-id :content "detail two")
|
||||
(let ((report (ebox-test--buffer-update-report)))
|
||||
(should (eq (plist-get report :strategy) 'span-patch))
|
||||
(should (equal (plist-get report :owner-ids) (list node-id)))
|
||||
(should-not (member root-id (plist-get report :owner-ids))))
|
||||
(should (ebox-test--buffer-visually-matches-runtime-render-p))))))
|
||||
|
||||
(ert-deftest ebox-rendered-owned-lines-trims-only-contiguous-outer-chrome ()
|
||||
"Role projection keeps owned lines and rejects an unowned interior gap."
|
||||
(let* ((owned-a (propertize "owned-a" 'ebox-content 7))
|
||||
(owned-b (propertize "owned-b" 'ebox-content 7))
|
||||
(projected
|
||||
(ebox-buffer--rendered-owned-lines
|
||||
(string-join (list "chrome-top" owned-a owned-b "chrome-bottom")
|
||||
"\n")
|
||||
'(7))))
|
||||
(should (equal (substring-no-properties projected)
|
||||
"owned-a\nowned-b"))
|
||||
(should-not
|
||||
(ebox-buffer--rendered-owned-lines
|
||||
(string-join (list owned-a "foreign-gap" owned-b) "\n") '(7)))
|
||||
(should-not
|
||||
(ebox-buffer--rendered-owned-lines
|
||||
(concat owned-a (propertize "foreign" 'ebox-content 8)) '(7)))
|
||||
(should
|
||||
(equal
|
||||
(substring-no-properties
|
||||
(ebox-buffer--rendered-owned-lines
|
||||
(string-join
|
||||
(list (propertize "border-top" 'ebox-bt 7)
|
||||
owned-a owned-b
|
||||
(propertize "border-bottom" 'ebox-bb 7))
|
||||
"\n")
|
||||
'(7) 2))
|
||||
"owned-a\nowned-b"))))
|
||||
|
||||
(ert-deftest ebox-flex-auto-size-content-update-refuses-current-slot ()
|
||||
"Auto-sized content must reflow instead of being forced into its old slot."
|
||||
(ebox-test--reset-runtime-state)
|
||||
|
||||
@ -841,6 +841,14 @@
|
||||
(ebox-flex-item source :flex-basis '(40)))))
|
||||
(should-not (ebox--flex-unrendered-measurement source props 'row))))
|
||||
|
||||
(ert-deftest ebox-flex-explicit-zero-min-width-allows-no-wrap-shrink ()
|
||||
"An explicit zero minimum lets a no-wrap card shrink in its Flex line."
|
||||
(let* ((source
|
||||
(ebox-create :content "A long no-wrap card title"
|
||||
:min-width 0 :wrap-mode nil))
|
||||
(rendered (ebox-render source)))
|
||||
(should (zerop (ebox--flex-box-min-main source rendered 'row)))))
|
||||
|
||||
(ert-deftest ebox-flex-bounded-auto-basis-keeps-intrinsic-measurement ()
|
||||
"Clipping must not replace an auto-basis item's intrinsic contribution."
|
||||
(let* ((source
|
||||
|
||||
@ -170,6 +170,33 @@
|
||||
(when (get-buffer " *other-window*")
|
||||
(kill-buffer " *other-window*")))))
|
||||
|
||||
(ert-deftest ebox-window-size-change-publishes-visible-viewport ()
|
||||
"A live frame resize updates the mounted surface's containing block."
|
||||
(let* ((buffer (generate-new-buffer " *ebox-window-size-change*"))
|
||||
(window (selected-window))
|
||||
(old-buffer (window-buffer window))
|
||||
calls)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(set-window-buffer window buffer)
|
||||
(cl-letf (((symbol-function 'ebox--buffer-render-state)
|
||||
(lambda (_buffer)
|
||||
'(:viewport-width 100 :viewport-height 10)))
|
||||
((symbol-function 'ebox-surface-buffer-mounted-p)
|
||||
(lambda (_buffer) t))
|
||||
((symbol-function 'ebox-surface--window-content-width)
|
||||
(lambda (_window) 240))
|
||||
((symbol-function 'window-body-height)
|
||||
(lambda (_window) 20))
|
||||
((symbol-function 'ebox-rerender-buffer-with-context)
|
||||
(lambda (target width height)
|
||||
(setq calls (list target width height))))
|
||||
(noninteractive nil))
|
||||
(ebox--window-size-change (selected-frame))
|
||||
(should (equal calls (list buffer 240 20)))))
|
||||
(set-window-buffer window old-buffer)
|
||||
(kill-buffer buffer))))
|
||||
|
||||
(defun ebox-surface-test--hash-fingerprint (table)
|
||||
"Return a stable content fingerprint for hash TABLE.
|
||||
The fingerprint checks entries rather than only table identity, so a failed
|
||||
@ -1227,6 +1254,27 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(should-not (equal (key-at nil t) (key-at 36 t)))
|
||||
(should-not (equal (key-at nil :unknown) (key-at 36 :unknown))))))
|
||||
|
||||
(ert-deftest ebox-flex-fragment-key-includes-rendered-region-ownership ()
|
||||
"Retained Flex output must not replay stale region text properties."
|
||||
(let* ((ebox--layout-fragments-table (make-hash-table :test 'equal))
|
||||
(source (ebox-create :content "stable" :width '(80)))
|
||||
(first-key
|
||||
(ebox-fragment-flex-allocation-key
|
||||
source 'row 80 1 'stretch 80
|
||||
'(:viewport-height-dependent nil)))
|
||||
(candidate (copy-tree source)))
|
||||
;; Candidate reconciliation can preserve a source node id while replacing
|
||||
;; one generated box region. The cached string embeds that region in its
|
||||
;; ownership properties, so node identity and geometry alone are unsafe.
|
||||
(plist-put candidate :region-id nil)
|
||||
(should (= (plist-get source :node-id)
|
||||
(plist-get candidate :node-id)))
|
||||
(should-not
|
||||
(equal first-key
|
||||
(ebox-fragment-flex-allocation-key
|
||||
candidate 'row 80 1 'stretch 80
|
||||
'(:viewport-height-dependent nil))))))
|
||||
|
||||
(ert-deftest ebox-flex-fragment-retention-evicts-one-entry-at-capacity ()
|
||||
"Fragment retention capacity must evict one old entry, not clear the table."
|
||||
(let ((ebox--layout-fragments-table (make-hash-table :test 'equal))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user