301 lines
12 KiB
EmacsLisp
301 lines
12 KiB
EmacsLisp
;;; ebox-fragment.el --- Layout fragments and snapshots for Ebox -*- lexical-binding: t; -*-
|
|
|
|
;;; Commentary:
|
|
;; Owns derived layout fragment and snapshot data used for diffing and
|
|
;; patchability checks. It does not own source identity or buffer edits.
|
|
|
|
;;; Code:
|
|
|
|
(require 'cl-lib)
|
|
(require 'subr-x)
|
|
(require 'ebox-tree)
|
|
(require 'ebox-measure)
|
|
|
|
(declare-function ebox--current-display-signature "ebox-measure" ())
|
|
(declare-function ebox--ensure-node-id "ebox" (node))
|
|
(declare-function ebox--complete-layout-snapshot
|
|
"ebox-incremental"
|
|
(buffer node snapshot &optional copy))
|
|
|
|
(defvar ebox--layout-fragments-table nil
|
|
"Dynamic published-generation layout fragment retention table.")
|
|
|
|
(defvar ebox--layout-fragments-reuse-p nil
|
|
"Non-nil when the current candidate may reuse retained layout fragments.")
|
|
|
|
(defconst ebox-fragment-flex-retention-max-entries 512
|
|
"Maximum number of retained final-sized Flex fragments per generation.")
|
|
|
|
(defvar ebox-fragment-flex-retention-hit-count 0
|
|
"Diagnostic count of retained final-sized Flex child fragment hits.")
|
|
|
|
(defvar ebox-fragment-flex-retention-rerender-count 0
|
|
"Diagnostic count of final-sized Flex child fragment rerenders.")
|
|
|
|
(defvar ebox-fragment-flex-retention-store-count 0
|
|
"Diagnostic count of retained final-sized Flex child fragment stores.")
|
|
|
|
(defun ebox-fragment-flex-allocation-key
|
|
(source axis main cross align viewport &optional render-context)
|
|
"Return exact retained-fragment key for SOURCE in Flex allocation context.
|
|
RENDER-CONTEXT contains layout-mode inputs that affect the rendered child but
|
|
are not part of its final allocated geometry."
|
|
(when (and (hash-table-p ebox--layout-fragments-table)
|
|
(listp source)
|
|
(not (stringp source))
|
|
(numberp viewport))
|
|
(let ((height-key
|
|
(if (and (plist-member render-context
|
|
:viewport-height-dependent)
|
|
(null (plist-get render-context
|
|
:viewport-height-dependent)))
|
|
: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
|
|
render-context
|
|
(and (fboundp 'ebox--current-display-signature)
|
|
(ebox--current-display-signature))))))
|
|
|
|
(defun ebox-fragment-flex-retained-side-effects-reusable-p (metadata)
|
|
"Return non-nil when retained fragment METADATA has no scroll effects.
|
|
Even a clear action is generation-sensitive: replaying or skipping it can
|
|
leave a stale scroll state after an A-to-B-to-A candidate sequence."
|
|
(and (listp metadata)
|
|
(not (plist-get metadata :stateful-p))
|
|
(plist-member metadata :scroll-actions)
|
|
(null (plist-get metadata :scroll-actions))))
|
|
|
|
(defun ebox-fragment-flex-retention-lookup (key)
|
|
"Return retained Flex fragment entry for KEY, or nil."
|
|
(when (and key (hash-table-p ebox--layout-fragments-table))
|
|
(let ((entry (gethash key ebox--layout-fragments-table)))
|
|
(when (and entry
|
|
ebox--layout-fragments-reuse-p
|
|
(ebox-fragment-flex-retained-side-effects-reusable-p
|
|
(plist-get entry :side-effects)))
|
|
(cl-incf ebox-fragment-flex-retention-hit-count)
|
|
(plist-get entry :entry)))))
|
|
|
|
(defun ebox-fragment-flex-retention-store (key entry side-effects)
|
|
"Store retained Flex fragment ENTRY under KEY with SIDE-EFFECTS."
|
|
(when (and key entry
|
|
(ebox-fragment-flex-retained-side-effects-reusable-p
|
|
side-effects)
|
|
(hash-table-p ebox--layout-fragments-table))
|
|
(when (and (not (gethash key ebox--layout-fragments-table))
|
|
(>= (hash-table-count ebox--layout-fragments-table)
|
|
ebox-fragment-flex-retention-max-entries))
|
|
(let (old-key)
|
|
(maphash
|
|
(lambda (candidate-key _old-entry)
|
|
(setq old-key candidate-key))
|
|
ebox--layout-fragments-table)
|
|
(when old-key
|
|
(remhash old-key ebox--layout-fragments-table))))
|
|
(puthash key
|
|
(list :entry entry :side-effects side-effects)
|
|
ebox--layout-fragments-table)
|
|
(cl-incf ebox-fragment-flex-retention-store-count))
|
|
entry)
|
|
|
|
(cl-defstruct (ebox-fragment
|
|
(:constructor ebox-fragment-create))
|
|
node-id type display region-ids child-ids style-signature size
|
|
line-signature span-footprint-signature external-footprint-signature
|
|
parent-slot-signature role-topology-signature overflow-signature
|
|
buffer-span buffer-spans)
|
|
|
|
(defun ebox-fragment-layout-signature (fragment)
|
|
"Return layout-significant fields from FRAGMENT.
|
|
Backend marker and buffer span data are intentionally excluded."
|
|
(let (signature)
|
|
(dolist (field '(:node-id :type :display :region-ids :child-ids
|
|
:style-signature :size :line-signature))
|
|
(let* ((name (intern (substring (symbol-name field) 1)))
|
|
(accessor (intern (format "ebox-fragment-%s" name)))
|
|
(value (and (fboundp accessor) (funcall accessor fragment))))
|
|
(when value
|
|
(setq signature (plist-put signature field value)))))
|
|
signature))
|
|
|
|
(defconst ebox--paint-style-signature-keys
|
|
'(:color :bgcolor
|
|
:border-top-p :border-bottom-p
|
|
:border-left-color :border-right-color
|
|
:border-top-color :border-bottom-color)
|
|
"Box properties that affect paint without changing layout geometry.")
|
|
|
|
(defconst ebox--geometry-style-signature-keys
|
|
'(:box-sizing
|
|
:width :min-width :max-width
|
|
:height :min-height :max-height
|
|
:padding-left-pixel :padding-right-pixel
|
|
:padding-top-height :padding-bottom-height
|
|
:margin-left-pixel :margin-right-pixel
|
|
:margin-top-height :margin-bottom-height
|
|
:border-left-pixel :border-right-pixel
|
|
:text-align :vertical-align
|
|
:overflow :wrap-mode
|
|
:flex-props :flex-participation)
|
|
"Style signature keys that require layout or owner rerender work.")
|
|
|
|
(defconst ebox--structure-style-signature-keys
|
|
'(:display)
|
|
"Style signature keys that change tree or formatting-context structure.")
|
|
|
|
(defconst ebox--style-signature-keys
|
|
(append ebox--paint-style-signature-keys
|
|
ebox--geometry-style-signature-keys)
|
|
"Box properties included in layout snapshot style signatures.")
|
|
|
|
(defun ebox-fragment-style-source-node (node)
|
|
"Return the box-like node carrying visual style for NODE."
|
|
(pcase (and (listp node) (plist-get node :ebox-type))
|
|
('box node)
|
|
('flex (plist-get node :box))
|
|
('grid (plist-get node :box))
|
|
(_ nil)))
|
|
|
|
(defun ebox-fragment-node-style-signature (node)
|
|
"Return NODE's display, layout, flex, and paint style signature."
|
|
(when (listp node)
|
|
(let ((signature (list :display (ebox--computed-display node))))
|
|
(when-let* ((style-node (ebox-fragment-style-source-node node)))
|
|
(dolist (key ebox--style-signature-keys)
|
|
(when (plist-member style-node key)
|
|
(setq signature
|
|
(plist-put signature key (plist-get style-node key)))))
|
|
(when-let* ((participation (ebox--flex-participation-props style-node)))
|
|
(setq signature
|
|
(plist-put signature :flex-participation participation))))
|
|
(when (eq (ebox--display-inner node) 'flex)
|
|
(setq signature
|
|
(plist-put signature :flex-props
|
|
(plist-get node :props))))
|
|
(when-let* ((participation (ebox--flex-participation-props node)))
|
|
(setq signature
|
|
(plist-put signature :flex-participation participation)))
|
|
signature)))
|
|
|
|
(defun ebox-fragment-plist-keys (plist)
|
|
"Return the keyword keys present in PLIST."
|
|
(let (keys)
|
|
(while plist
|
|
(push (pop plist) keys)
|
|
(pop plist))
|
|
(nreverse keys)))
|
|
|
|
(defun ebox-fragment-plist-changed-keys (old new)
|
|
"Return keys whose plist values differ between OLD and NEW."
|
|
(let (changed keys)
|
|
(dolist (key (append (ebox-fragment-plist-keys old)
|
|
(ebox-fragment-plist-keys new)))
|
|
(cl-pushnew key keys))
|
|
(dolist (key keys)
|
|
(unless (equal (plist-get old key)
|
|
(plist-get new key))
|
|
(push key changed)))
|
|
changed))
|
|
|
|
(defun ebox-fragment-style-signature-dirty-kind (old new)
|
|
"Return dirty kind implied by OLD and NEW style signatures."
|
|
(let ((changed (ebox-fragment-plist-changed-keys old new)))
|
|
(cond
|
|
((null changed) nil)
|
|
((cl-some (lambda (key)
|
|
(memq key ebox--structure-style-signature-keys))
|
|
changed)
|
|
'structure)
|
|
((cl-some (lambda (key)
|
|
(memq key ebox--geometry-style-signature-keys))
|
|
changed)
|
|
'geometry)
|
|
(t 'paint))))
|
|
|
|
(defun ebox-fragment-layout-snapshot-detailed-p (snapshot)
|
|
"Return non-nil when SNAPSHOT already has expensive detail fields."
|
|
(and (plist-member snapshot :buffer-spans)
|
|
(plist-member snapshot :line-signature)
|
|
(plist-member snapshot :span-footprint-signature)
|
|
(plist-member snapshot :external-footprint-signature)
|
|
(plist-member snapshot :parent-slot-signature)
|
|
(plist-member snapshot :role-topology-signature)
|
|
(plist-member snapshot :overflow-signature)))
|
|
|
|
(defun ebox-fragment-layout-snapshot-spans-p (snapshot)
|
|
"Return non-nil when SNAPSHOT already has buffer span details."
|
|
(plist-member snapshot :buffer-spans))
|
|
|
|
(defun ebox-fragment-node-layout-snapshot (buffer node &optional details)
|
|
"Return a derived layout snapshot for NODE in BUFFER.
|
|
The default snapshot is intentionally lightweight; when DETAILS is non-nil,
|
|
include buffer spans and line signatures."
|
|
(let* ((node-id (ebox--ensure-node-id node))
|
|
(region-ids (ebox--node-all-region-ids node))
|
|
(snapshot
|
|
(list :node-id node-id
|
|
:type (plist-get node :ebox-type)
|
|
:display (ebox--computed-display node)
|
|
:region-ids region-ids
|
|
:style-signature (ebox--node-style-signature node)
|
|
:child-ids
|
|
(mapcar #'ebox--ensure-node-id
|
|
(ebox--node-children node)))))
|
|
(if details
|
|
(ebox--complete-layout-snapshot buffer node snapshot)
|
|
snapshot)))
|
|
|
|
(defun ebox-fragment-layout-snapshot-dirty-kind (old new)
|
|
"Return the dirty kind between OLD and NEW layout snapshots."
|
|
(cond
|
|
((or (null old) (null new)) 'structure)
|
|
((not (eq (plist-get old :type)
|
|
(plist-get new :type)))
|
|
'structure)
|
|
((not (equal (plist-get old :display)
|
|
(plist-get new :display)))
|
|
'structure)
|
|
((not (equal (plist-get old :child-ids)
|
|
(plist-get new :child-ids)))
|
|
'structure)
|
|
((not (equal (plist-get old :line-signature)
|
|
(plist-get new :line-signature)))
|
|
'geometry)
|
|
((not (equal (plist-get old :buffer-spans)
|
|
(plist-get new :buffer-spans)))
|
|
'placement)
|
|
((not (equal (plist-get old :style-signature)
|
|
(plist-get new :style-signature)))
|
|
(ebox-fragment-style-signature-dirty-kind
|
|
(plist-get old :style-signature)
|
|
(plist-get new :style-signature)))))
|
|
|
|
(defalias 'ebox--style-source-node #'ebox-fragment-style-source-node)
|
|
(defalias 'ebox--node-style-signature #'ebox-fragment-node-style-signature)
|
|
(defalias 'ebox--plist-keys #'ebox-fragment-plist-keys)
|
|
(defalias 'ebox--plist-changed-keys #'ebox-fragment-plist-changed-keys)
|
|
(defalias 'ebox--style-signature-dirty-kind
|
|
#'ebox-fragment-style-signature-dirty-kind)
|
|
(defalias 'ebox--layout-snapshot-detailed-p
|
|
#'ebox-fragment-layout-snapshot-detailed-p)
|
|
(defalias 'ebox--layout-snapshot-spans-p
|
|
#'ebox-fragment-layout-snapshot-spans-p)
|
|
(defalias 'ebox--node-layout-snapshot
|
|
#'ebox-fragment-node-layout-snapshot)
|
|
(defalias 'ebox--layout-snapshot-dirty-kind
|
|
#'ebox-fragment-layout-snapshot-dirty-kind)
|
|
(defalias 'ebox--flex-fragment-allocation-key
|
|
#'ebox-fragment-flex-allocation-key)
|
|
(defalias 'ebox--flex-fragment-retention-lookup
|
|
#'ebox-fragment-flex-retention-lookup)
|
|
(defalias 'ebox--flex-fragment-retention-store
|
|
#'ebox-fragment-flex-retention-store)
|
|
|
|
(provide 'ebox-fragment)
|
|
|
|
;;; ebox-fragment.el ends here
|