refactor: enforce canonical Text and Box runtime boundaries
This commit is contained in:
parent
f9050cee72
commit
0c7e7b974a
17
Makefile
17
Makefile
@ -5,6 +5,7 @@ ECSS_DIR ?= ../ecss
|
||||
LOAD_EXTRA ?=
|
||||
|
||||
EMACS_BATCH = $(EMACS) -Q --batch -L . -L $(ECSS_DIR) -L $(TP_DIR) $(LOAD_EXTRA) --eval '(setq load-prefer-newer t)'
|
||||
EMACS_TEST = $(EMACS_BATCH) -L tests -l tests/ebox-fixtures.el
|
||||
NATIVE_MANIFEST = native/Cargo.toml
|
||||
NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))')
|
||||
NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release
|
||||
@ -27,34 +28,34 @@ compile:
|
||||
test: core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests
|
||||
|
||||
core-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-core-render-tests.el -f ert-run-tests-batch-and-exit
|
||||
$(EMACS_TEST) -l tests/ebox-core-render-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
child-range-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-child-range-tests.el -f ert-run-tests-batch-and-exit
|
||||
$(EMACS_TEST) -l tests/ebox-child-range-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
grid-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-grid-tests.el -f ert-run-tests-batch-and-exit
|
||||
$(EMACS_TEST) -l tests/ebox-grid-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
ebox-commit-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-commit-tests.el -f ert-run-tests-batch-and-exit
|
||||
$(EMACS_TEST) -l tests/ebox-commit-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
surface-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-surface-tests.el -f ert-run-tests-batch-and-exit
|
||||
$(EMACS_TEST) -l tests/ebox-surface-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
visual-check-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-visual-check-tests.el -f ert-run-tests-batch-and-exit
|
||||
$(EMACS_TEST) -l tests/ebox-visual-check-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
package-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-package-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
selector-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-selector-tests.el -f ert-run-tests-batch-and-exit
|
||||
$(EMACS_TEST) -l tests/ebox-selector-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
dsl-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-dsl-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
flex-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-flex-tests.el -f ert-run-tests-batch-and-exit
|
||||
$(EMACS_TEST) -l tests/ebox-flex-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
performance-evaluator: surface-tests ebox-commit-tests
|
||||
$(EMACS_BATCH) -l scripts/ebox-performance-evaluator.el -f ebox-performance-evaluator-batch
|
||||
|
||||
@ -191,7 +191,7 @@ together.
|
||||
| Border | `:border`, `:border-top`, `:border-right`, `:border-bottom`, `:border-left`, `:border-width`, `:border-style`, `:border-color`; side longhands `:border-*-width`, `:border-*-style`, `:border-*-color`; Ebox boolean shorthands `:border-top-p`, `:border-bottom-p`; compatibility aliases `:border-left-pixel`, `:border-right-pixel` |
|
||||
| Paint | `:color`, `:background-color`, and alias `:bgcolor` |
|
||||
| Typography | `:font`, `:font-family`, `:font-height` and alias `:font-size`, `:font-weight`, `:font-slant` |
|
||||
| Text/layout | `:text-align` (`left`, `center`, `right`), `:vertical-align` (`top`, `center`/`middle`, `bottom`), `:overflow` (`scroll`, `hidden`, `visible`), `:wrap-mode` (`word`, `char`, `kp`, or nil), `:visibility` (`visible` or `hidden`) |
|
||||
| Box content | `:text-align` (`left`, `center`, `right`), `:vertical-align` (`top`, `center`/`middle`, `bottom`), `:overflow` (`scroll`, `hidden`, `visible`), `:wrap-mode` (`word`, `char`, `kp`, or `none`; default `word`), `:visibility` (`visible` or `hidden`) |
|
||||
| Structural style | `:display` is computed for node/layout contexts; the layout constructors normally choose the valid display tuple. |
|
||||
| Flex container/item | `:flex-direction`, `:flex-wrap`, `:flex-flow`, `:justify-content`, `:align-items`, `:align-content`, `:gap`, `:row-gap`, `:column-gap` (aliases `:grid-row-gap`, `:grid-column-gap`); item names `:order`, `:flex`, `:flex-grow`, `:flex-shrink`, `:flex-basis`, `:align-self` |
|
||||
| Grid container/item | `:grid-template-columns`, `:grid-template-rows`, `:grid-auto-columns`, `:grid-auto-rows`, `:grid-auto-flow`, `:justify-items`; item placement `:grid-column`, `:grid-row`, `:grid-column-span`, `:grid-row-span` |
|
||||
|
||||
@ -174,7 +174,7 @@ property;`ebox-region-update` 接受其目标支持的可变 content/style/scr
|
||||
| Border | `:border`、`:border-top`、`:border-right`、`:border-bottom`、`:border-left`、`:border-width`、`:border-style`、`:border-color`;side longhand `:border-*-width`、`:border-*-style`、`:border-*-color`;Ebox boolean shorthand `:border-top-p`、`:border-bottom-p`;兼容别名 `:border-left-pixel`、`:border-right-pixel` |
|
||||
| Paint | `:color`、`:background-color`、别名 `:bgcolor` |
|
||||
| Typography | `:font`、`:font-family`、`:font-height` 和别名 `:font-size`、`:font-weight`、`:font-slant` |
|
||||
| Text/layout | `:text-align`(`left`、`center`、`right`)、`:vertical-align`(`top`、`center`/`middle`、`bottom`)、`:overflow`(`scroll`、`hidden`、`visible`)、`:wrap-mode`(`word`、`char`、`kp` 或 nil)、`:visibility`(`visible` 或 `hidden`) |
|
||||
| Box 内容 | `:text-align`(`left`、`center`、`right`)、`:vertical-align`(`top`、`center`/`middle`、`bottom`)、`:overflow`(`scroll`、`hidden`、`visible`)、`:wrap-mode`(`word`、`char`、`kp`、`none`,默认 `word`)、`:visibility`(`visible` 或 `hidden`) |
|
||||
| Structural style | `:display` 会在 node/layout context 中计算;有效的 display tuple 通常由布局构造器选择。 |
|
||||
| Flex container/item | `:flex-direction`、`:flex-wrap`、`:flex-flow`、`:justify-content`、`:align-items`、`:align-content`、`:gap`、`:row-gap`、`:column-gap`(别名 `:grid-row-gap`、`:grid-column-gap`);item 名称 `:order`、`:flex`、`:flex-grow`、`:flex-shrink`、`:flex-basis`、`:align-self` |
|
||||
| Grid container/item | `:grid-template-columns`、`:grid-template-rows`、`:grid-auto-columns`、`:grid-auto-rows`、`:grid-auto-flow`、`:justify-items`;item placement `:grid-column`、`:grid-row`、`:grid-column-span`、`:grid-row-span` |
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
(require 'ebox-layout-config)
|
||||
(require 'ebox-style)
|
||||
(require 'ebox-node-factory)
|
||||
(require 'ebox-child-range)
|
||||
|
||||
(defun ebox-canonical--validate-plist (plist context)
|
||||
"Validate PLIST shape for error CONTEXT and return PLIST."
|
||||
@ -52,16 +53,40 @@
|
||||
(error "%s accepts %S at most once" context field))
|
||||
(and values (cons t (car values)))))
|
||||
|
||||
(defun ebox-canonical--compiled-property-definitions (plist context)
|
||||
"Return canonical property definitions compiled from PLIST for CONTEXT."
|
||||
(condition-case err
|
||||
(cl-loop for (property _value)
|
||||
on (ebox-style-compile-declarations plist t) by #'cddr
|
||||
collect (or (ebox-style-property property)
|
||||
(error "Missing canonical Ebox property: %S"
|
||||
property)))
|
||||
(error
|
||||
(error "%s rejected properties: %s" context (error-message-string err)))))
|
||||
(defconst ebox-canonical--source-metadata-fields
|
||||
'(:source-handle :key :class :id)
|
||||
"Source facts accepted by both canonical Text and Box constructors.")
|
||||
|
||||
(defvar ebox-canonical--declaration-fact-cache
|
||||
(make-hash-table :test #'eq :weakness 'key)
|
||||
"Declaration-identity keyed validated canonical node facts.")
|
||||
|
||||
(defun ebox-canonical--source-metadata (plist context)
|
||||
"Return detached source metadata from PLIST for CONTEXT.
|
||||
Every metadata field is optional but may occur at most once."
|
||||
(let (metadata)
|
||||
(dolist (field ebox-canonical--source-metadata-fields)
|
||||
(when-let* ((entry (ebox-canonical--optional-field
|
||||
plist field context)))
|
||||
(setq metadata
|
||||
(append metadata (list field (cdr entry))))))
|
||||
metadata))
|
||||
|
||||
(defun ebox-canonical--apply-source-metadata (node metadata)
|
||||
"Attach canonical source METADATA to runtime NODE and return NODE."
|
||||
(while metadata
|
||||
(let ((field (pop metadata))
|
||||
(value (pop metadata)))
|
||||
(plist-put node
|
||||
(pcase field
|
||||
(:source-handle :ebox-source-handle)
|
||||
(_ field))
|
||||
value)
|
||||
;; `:host-ref' is the private retained-geometry projection of the
|
||||
;; source-owned handle. It is not a second public identity field.
|
||||
(when (and (eq field :source-handle) value)
|
||||
(plist-put node :host-ref value))))
|
||||
node)
|
||||
|
||||
(defun ebox-canonical--text-measurement-property-p (property)
|
||||
"Return non-nil when PROPERTY belongs to canonical Text measurement."
|
||||
@ -70,20 +95,81 @@
|
||||
(defun ebox-canonical--box-frame-property-p (property)
|
||||
"Return non-nil when PROPERTY belongs to Box geometry or participation."
|
||||
(let ((contexts (plist-get property :contexts))
|
||||
(group (plist-get property :group))
|
||||
(name (plist-get property :name)))
|
||||
(group (plist-get property :group)))
|
||||
(or (memq 'item contexts)
|
||||
(and (memq 'box contexts)
|
||||
(not (memq group '(paint typography)))
|
||||
(not (eq name :wrap-mode))))))
|
||||
(not (memq group '(paint typography)))))))
|
||||
|
||||
(defun ebox-canonical--assert-property-role (plist predicate context)
|
||||
"Require every compiled PLIST property to satisfy PREDICATE for CONTEXT."
|
||||
(dolist (property
|
||||
(ebox-canonical--compiled-property-definitions plist context))
|
||||
(unless (funcall predicate property)
|
||||
(error "%s does not accept %S"
|
||||
context (plist-get property :name)))))
|
||||
(defun ebox-canonical--node-properties
|
||||
(props declarations-field predicate context tag)
|
||||
"Return declarations, engine props, and projection need for one node.
|
||||
PROPS are direct typed inputs. DECLARATIONS-FIELD is an optional normalized
|
||||
author fact set. PREDICATE selects canonical node-owned values for CONTEXT
|
||||
and author TAG."
|
||||
(if declarations-field
|
||||
(progn
|
||||
(when props
|
||||
(error "%s cannot mix normalized declarations with direct properties"
|
||||
context))
|
||||
(let* ((declarations (cdr declarations-field))
|
||||
(by-tag
|
||||
(or (gethash declarations
|
||||
ebox-canonical--declaration-fact-cache)
|
||||
(let ((table (make-hash-table :test #'eq)))
|
||||
(puthash declarations table
|
||||
ebox-canonical--declaration-fact-cache)
|
||||
table)))
|
||||
(missing (make-symbol "ebox-canonical-facts-missing"))
|
||||
(cached (gethash tag by-tag missing)))
|
||||
(if (not (eq cached missing))
|
||||
cached
|
||||
(ebox-style-validate-form-declarations tag declarations)
|
||||
(let ((facts
|
||||
(list declarations
|
||||
(ebox-style-declaration-properties
|
||||
declarations
|
||||
(lambda (property)
|
||||
(or (funcall predicate property)
|
||||
(memq (plist-get property :group)
|
||||
'(paint typography)))))
|
||||
(ebox-style-declarations-require-projection-p
|
||||
declarations
|
||||
(lambda (property)
|
||||
(or (funcall predicate property)
|
||||
(memq (plist-get property :group)
|
||||
'(paint typography))
|
||||
(and (memq tag '(flex grid))
|
||||
(memq tag
|
||||
(plist-get property
|
||||
:contexts)))))))))
|
||||
(setf (nth 1 facts)
|
||||
(ebox-style-expand-ebox-plist (nth 1 facts)))
|
||||
(puthash tag facts by-tag)
|
||||
facts))))
|
||||
(let ((declarations
|
||||
(if (null props)
|
||||
nil
|
||||
(condition-case err
|
||||
(ebox-style-compile-declarations props t)
|
||||
(error
|
||||
(error "%s rejected properties: %s"
|
||||
context (error-message-string err)))))))
|
||||
(cl-loop for (id _value) on declarations by #'cddr
|
||||
for property = (or (ebox-style-property id)
|
||||
(error "Missing canonical Ebox property: %S"
|
||||
id))
|
||||
unless (funcall predicate property)
|
||||
do (error "%s does not accept %S"
|
||||
context (plist-get property :name)))
|
||||
(list declarations
|
||||
(ebox-style-expand-ebox-plist
|
||||
(ebox-style-declaration-properties
|
||||
declarations
|
||||
(lambda (property)
|
||||
(or (funcall predicate property)
|
||||
(memq (plist-get property :group)
|
||||
'(paint typography))))))
|
||||
nil))))
|
||||
|
||||
(defun ebox-node-kind (node)
|
||||
"Return canonical NODE kind, or nil for a legacy runtime node."
|
||||
@ -114,34 +200,93 @@
|
||||
(ebox-layout-config--copy (plist-get node :ebox-layout-config)))
|
||||
|
||||
(defun ebox-box-node-children (node)
|
||||
"Return canonical BoxNode NODE's retained canonical children."
|
||||
"Return canonical BoxNode NODE's material Text/Box children."
|
||||
(unless (ebox-box-node-p node)
|
||||
(error "Expected canonical BoxNode, got %S" node))
|
||||
(plist-get node :children))
|
||||
(if (plist-member node :children)
|
||||
(plist-get node :children)
|
||||
(when-let* ((sequence (plist-get node :ebox-child-sequence)))
|
||||
(ebox-child-range--flatten sequence))))
|
||||
|
||||
(defun ebox-box-node-range-anchors (node)
|
||||
"Return transparent Range anchors retained by canonical BoxNode NODE."
|
||||
(unless (ebox-box-node-p node)
|
||||
(error "Expected canonical BoxNode, got %S" node))
|
||||
(let ((sequence (plist-get node :ebox-child-sequence))
|
||||
(offset 0)
|
||||
anchors)
|
||||
(when sequence
|
||||
(dotimes (index (ebox-child-range--sequence-count sequence))
|
||||
(let* ((segment (ebox-child-range--segment-at sequence index))
|
||||
(payload (ebox-child-range--segment-payload segment))
|
||||
(after (+ offset (length payload))))
|
||||
(when-let* ((ref (ebox-child-range--segment-ref segment)))
|
||||
(push (list :ref ref :before offset :after after) anchors))
|
||||
(setq offset after))))
|
||||
(nreverse anchors)))
|
||||
|
||||
(defun ebox-canonical--normalize-children (children)
|
||||
"Return material children and optional Range sequence for CHILDREN."
|
||||
(let (material segments range-p)
|
||||
(dolist (entry children)
|
||||
(if (ebox-child-range--descriptor-p entry)
|
||||
(let ((items (ebox-child-range--descriptor-items entry)))
|
||||
(unless (cl-every (lambda (item)
|
||||
(or (ebox-text-node-p item)
|
||||
(ebox-box-node-p item)))
|
||||
items)
|
||||
(error "Ebox Box Range items must be canonical Text/Box nodes: %S"
|
||||
items))
|
||||
(setq range-p t)
|
||||
(push (cons (ebox-child-range--descriptor-ref entry) items)
|
||||
segments)
|
||||
(dolist (item items)
|
||||
(push item material)))
|
||||
(unless (or (ebox-text-node-p entry) (ebox-box-node-p entry))
|
||||
(error "Ebox Box children must be canonical Text/Box nodes or Ranges: %S"
|
||||
children))
|
||||
(push (cons nil (list entry)) segments)
|
||||
(push entry material)))
|
||||
(list (nreverse material)
|
||||
(and range-p
|
||||
(ebox-child-range--build (nreverse segments) nil t)))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-text-create (&rest plist)
|
||||
"Create a canonical TextNode from evaluated PLIST.
|
||||
|
||||
`:value' is required and must be one string. `:source-handle' is optional.
|
||||
Only text measurement properties are accepted; paint is projected separately."
|
||||
`:value' is required and must be one string. `:source-handle', `:key',
|
||||
`:class', and `:id' are optional source facts. Direct properties are limited
|
||||
to text measurement. `:declarations' may carry one already-normalized Text
|
||||
author fact set so paint is projected without reparsing."
|
||||
(ebox-canonical--validate-plist plist "ebox-text-create")
|
||||
(let* ((value (ebox-canonical--required-field
|
||||
plist :value "Ebox Text"))
|
||||
(source-field (ebox-canonical--optional-field
|
||||
plist :source-handle "Ebox Text"))
|
||||
(source-handle (cdr source-field))
|
||||
(declarations-field (ebox-canonical--optional-field
|
||||
plist :declarations "Ebox Text"))
|
||||
(source-metadata (ebox-canonical--source-metadata plist "Ebox Text"))
|
||||
(props (ebox-canonical--without-keys
|
||||
plist '(:value :source-handle))))
|
||||
plist (append '(:value :declarations)
|
||||
ebox-canonical--source-metadata-fields))))
|
||||
(unless (stringp value)
|
||||
(error "Ebox Text :value must be a string: %S" value))
|
||||
(ebox-canonical--assert-property-role
|
||||
props #'ebox-canonical--text-measurement-property-p "Ebox Text")
|
||||
(let ((node (apply #'ebox-create (append props (list :content value)))))
|
||||
(pcase-let* ((`(,declarations ,engine-props ,style-required-p)
|
||||
(ebox-canonical--node-properties
|
||||
props declarations-field
|
||||
#'ebox-canonical--text-measurement-property-p
|
||||
"Ebox Text" 'text))
|
||||
(node
|
||||
(ebox-node-factory--create-text-expanded
|
||||
(append engine-props (list :content value))
|
||||
declarations)))
|
||||
(plist-put node :ebox-kind 'text)
|
||||
(plist-put node :ebox-text-value value)
|
||||
(plist-put node :ebox-source-handle source-handle)
|
||||
(ebox-canonical--apply-source-metadata node source-metadata)
|
||||
(plist-put node :display '(inline flow))
|
||||
(plist-put node :ebox-author-style-count
|
||||
(if style-required-p 1 0))
|
||||
(when style-required-p
|
||||
(plist-put node :ebox-author-style-required-p t))
|
||||
node)))
|
||||
|
||||
;;;###autoload
|
||||
@ -150,21 +295,24 @@
|
||||
|
||||
`:layout' must occur exactly once as an `ebox-layout-config'.
|
||||
`:children' is a list of canonical nodes. Every Layout accepts zero or more
|
||||
children according to its own formatting algorithm."
|
||||
children according to its own formatting algorithm. `:declarations' may
|
||||
carry one already-normalized Box author fact set."
|
||||
(ebox-canonical--validate-plist plist "ebox-box-create")
|
||||
(let* ((layout (ebox-canonical--required-field
|
||||
plist :layout "Ebox Box"))
|
||||
(children-field (ebox-canonical--optional-field
|
||||
plist :children "Ebox Box"))
|
||||
(declarations-field (ebox-canonical--optional-field
|
||||
plist :declarations "Ebox Box"))
|
||||
(outer-field (ebox-canonical--optional-field
|
||||
plist :outer "Ebox Box"))
|
||||
(source-field (ebox-canonical--optional-field
|
||||
plist :source-handle "Ebox Box"))
|
||||
(source-metadata (ebox-canonical--source-metadata plist "Ebox Box"))
|
||||
(children (if children-field (cdr children-field) nil))
|
||||
child-sequence
|
||||
(outer (if outer-field (cdr outer-field) 'block))
|
||||
(source-handle (cdr source-field))
|
||||
(props (ebox-canonical--without-keys
|
||||
plist '(:layout :children :outer :source-handle))))
|
||||
plist (append '(:layout :children :outer :declarations)
|
||||
ebox-canonical--source-metadata-fields))))
|
||||
(unless (ebox-layout-config-p layout)
|
||||
(error "Ebox Box :layout must be a typed layout config: %S"
|
||||
layout))
|
||||
@ -173,31 +321,47 @@ children according to its own formatting algorithm."
|
||||
(error "Ebox Box :outer must be inline or block: %S" outer))
|
||||
(unless (proper-list-p children)
|
||||
(error "Ebox Box :children must be a proper list: %S" children))
|
||||
(unless (cl-every (lambda (child)
|
||||
(or (ebox-text-node-p child)
|
||||
(ebox-box-node-p child)))
|
||||
children)
|
||||
(error "Ebox Box children must be canonical Text/Box nodes: %S"
|
||||
children))
|
||||
(pcase-let ((`(,material-children ,normalized-sequence)
|
||||
(ebox-canonical--normalize-children children)))
|
||||
(setq children material-children)
|
||||
(setq child-sequence normalized-sequence))
|
||||
(unless (memq (ebox-layout-config-kind layout)
|
||||
'(normal row column flex grid))
|
||||
(error "Ebox Box Layout is not implemented: %S"
|
||||
(ebox-layout-config-kind layout)))
|
||||
(ebox-canonical--assert-property-role
|
||||
props #'ebox-canonical--box-frame-property-p "Ebox Box")
|
||||
(let ((node (apply #'ebox-create props)))
|
||||
(pcase-let* ((tag (if (eq (ebox-layout-config-kind layout) 'normal)
|
||||
'box
|
||||
(ebox-layout-config-kind layout)))
|
||||
(`(,declarations ,engine-props ,style-required-p)
|
||||
(ebox-canonical--node-properties
|
||||
props declarations-field
|
||||
#'ebox-canonical--box-frame-property-p
|
||||
"Ebox Box" tag))
|
||||
(node (ebox-node-factory--create-expanded
|
||||
engine-props declarations)))
|
||||
(plist-put node :ebox-kind 'box)
|
||||
(plist-put node :ebox-layout-config layout)
|
||||
(plist-put node :children children)
|
||||
(plist-put node :ebox-source-handle source-handle)
|
||||
;; Children own their text formatting. The Box frame consumes their
|
||||
;; already-laid-out output and must not wrap or justify it a second time.
|
||||
(plist-put node :wrap-mode nil)
|
||||
(when child-sequence
|
||||
(plist-put node :ebox-child-sequence child-sequence))
|
||||
(ebox-canonical--apply-source-metadata node source-metadata)
|
||||
(plist-put node :display
|
||||
(list outer
|
||||
(pcase (ebox-layout-config-kind layout)
|
||||
('normal 'flow)
|
||||
(kind kind))))
|
||||
(let ((style-count
|
||||
(+ (if style-required-p 1 0)
|
||||
(cl-loop for child in children
|
||||
sum (or (plist-get child
|
||||
:ebox-author-style-count)
|
||||
(if (plist-get
|
||||
child :ebox-author-style-required-p)
|
||||
1
|
||||
0))))))
|
||||
(plist-put node :ebox-author-style-count style-count)
|
||||
(when (> style-count 0)
|
||||
(plist-put node :ebox-author-style-required-p t)))
|
||||
node)))
|
||||
|
||||
(provide 'ebox-canonical)
|
||||
|
||||
@ -185,16 +185,20 @@ VALUE is the replacement segment and METRICS records all path work."
|
||||
(cdr (assoc key (and node (ebox-child-range--hash-node-bucket node))
|
||||
#'equal))))
|
||||
|
||||
(defun ebox-child-range--item-copy (item metrics)
|
||||
"Validate and copy declarative ITEM while recording METRICS."
|
||||
(defun ebox-child-range--item-copy (item metrics &optional retain-identity-p)
|
||||
"Validate declarative ITEM while recording METRICS.
|
||||
Return ITEM itself when RETAIN-IDENTITY-P is non-nil; otherwise return a
|
||||
shallow copy owned by the persistent sequence."
|
||||
(when (ebox-child-range--descriptor-p item)
|
||||
(error "Nested child Range descriptors are reserved"))
|
||||
(unless (and (listp item) (not (stringp item)))
|
||||
(error "Ebox child Range item must be one declarative node"))
|
||||
(ebox-child-range--metric-add metrics 'new-payload-validations)
|
||||
(ebox-child-range--metric-add metrics 'new-payload-visits)
|
||||
(ebox-child-range--metric-add metrics 'new-payload-copies)
|
||||
(copy-sequence item))
|
||||
(if retain-identity-p
|
||||
item
|
||||
(ebox-child-range--metric-add metrics 'new-payload-copies)
|
||||
(copy-sequence item)))
|
||||
|
||||
(defun ebox-child-range--descriptor-create (ref items)
|
||||
"Return a reserved Range descriptor for non-nil REF and list ITEMS."
|
||||
@ -203,10 +207,13 @@ VALUE is the replacement segment and METRICS records all path work."
|
||||
(signal 'wrong-type-argument (list 'proper-list-p items)))
|
||||
(ebox-child-range--make-descriptor :ref ref :items items))
|
||||
|
||||
(defun ebox-child-range--build (segments &optional hash-function)
|
||||
(defun ebox-child-range--build
|
||||
(segments &optional hash-function retain-item-identities-p)
|
||||
"Build an immutable child sequence from SEGMENTS.
|
||||
Each entry is `(REF . ITEMS)'; nil REF denotes one static child item.
|
||||
HASH-FUNCTION optionally supplies the stable key hash."
|
||||
HASH-FUNCTION optionally supplies the stable key hash. When
|
||||
RETAIN-ITEM-IDENTITIES-P is non-nil, ITEMS are already-owned canonical nodes
|
||||
and the sequence retains those exact objects instead of copying them."
|
||||
(unless (proper-list-p segments)
|
||||
(signal 'wrong-type-argument (list 'proper-list-p segments)))
|
||||
(let* ((count (length segments))
|
||||
@ -227,7 +234,8 @@ HASH-FUNCTION optionally supplies the stable key hash."
|
||||
(when ref (puthash ref index refs))
|
||||
(let ((payload (vconcat
|
||||
(mapcar (lambda (item)
|
||||
(ebox-child-range--item-copy item metrics))
|
||||
(ebox-child-range--item-copy
|
||||
item metrics retain-item-identities-p))
|
||||
items))))
|
||||
(setq root (ebox-child-range--segment-insert
|
||||
root height index
|
||||
|
||||
418
ebox-dsl.el
418
ebox-dsl.el
@ -1,27 +1,166 @@
|
||||
;;; ebox-dsl.el --- DSL compiler facade for Ebox -*- lexical-binding: t; -*-
|
||||
;;; ebox-dsl.el --- Canonical author DSL for Ebox -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
;; Owns `.ebox` DSL compilation and compatibility sugar over the Ebox element
|
||||
;; and style models. It does not own core render or buffer patch execution.
|
||||
;; Owns the complete inert `.ebox' author boundary: form parsing, form-local
|
||||
;; property validation, and desugaring into canonical TextNode/BoxNode plus
|
||||
;; typed LayoutConfig values. It does not own layout, paint materialization,
|
||||
;; buffer publication, or framework runtime identity.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'ebox-style)
|
||||
(require 'ebox-tree)
|
||||
(require 'ebox-canonical)
|
||||
(require 'ebox-flex)
|
||||
(require 'ebox-grid)
|
||||
(require 'ebox-canonical)
|
||||
|
||||
(declare-function ebox-create "ebox" (&rest plist))
|
||||
(cl-defstruct
|
||||
(ebox-dsl--properties
|
||||
(:constructor ebox-dsl--make-properties))
|
||||
"Normalized properties for one canonical Text or Box author source."
|
||||
tag
|
||||
source-metadata
|
||||
outer
|
||||
layout
|
||||
declarations)
|
||||
|
||||
(defconst ebox-dsl--outer-values '(inline block)
|
||||
"Valid outer participation values for the canonical Box DSL.")
|
||||
(defconst ebox-dsl--box-tags
|
||||
'(box row column flex grid)
|
||||
"Author tags that construct one canonical BoxNode.")
|
||||
|
||||
(defconst ebox-dsl--layout-values '(normal row column flex grid)
|
||||
"Valid child layout values for the canonical Box DSL.")
|
||||
(defconst ebox-dsl--source-fields
|
||||
'(:key :class :id)
|
||||
"Shared source metadata accepted by Ebox author surfaces.")
|
||||
|
||||
(defun ebox--dsl-unquote-value (value)
|
||||
(defun ebox-dsl--source-handle ()
|
||||
"Return one opaque identity for a normalized author node."
|
||||
(make-symbol "ebox-source"))
|
||||
|
||||
(defun ebox-dsl--validate-plist (plist context)
|
||||
"Validate PLIST shape for CONTEXT and return PLIST."
|
||||
(unless (and (proper-list-p plist) (zerop (% (length plist) 2)))
|
||||
(error "%s properties must be an even plist: %S" context plist))
|
||||
(cl-loop for key in plist by #'cddr
|
||||
unless (keywordp key)
|
||||
do (error "%s property name must be a keyword: %S" context key))
|
||||
plist)
|
||||
|
||||
(defun ebox-dsl--field-values (plist field)
|
||||
"Return every FIELD value from PLIST in source order."
|
||||
(cl-loop for (key value) on plist by #'cddr
|
||||
when (eq key field)
|
||||
collect value))
|
||||
|
||||
(defun ebox-dsl--optional-field (plist field context)
|
||||
"Return optional FIELD from PLIST, rejecting duplicates for CONTEXT."
|
||||
(let ((values (ebox-dsl--field-values plist field)))
|
||||
(when (> (length values) 1)
|
||||
(error "%s accepts %S at most once" context field))
|
||||
(and values (cons t (car values)))))
|
||||
|
||||
(defun ebox-dsl--without-keys (plist keys)
|
||||
"Return PLIST without entries whose keys occur in KEYS."
|
||||
(cl-loop for (key value) on plist by #'cddr
|
||||
unless (memq key keys)
|
||||
append (list key value)))
|
||||
|
||||
(defun ebox-dsl--keep-keys (plist keys)
|
||||
"Return PLIST entries whose keys occur in KEYS."
|
||||
(cl-loop for (key value) on plist by #'cddr
|
||||
when (memq key keys)
|
||||
append (list key value)))
|
||||
|
||||
(defun ebox-dsl--source-metadata
|
||||
(properties source-handle context)
|
||||
"Return source metadata from PROPERTIES and SOURCE-HANDLE for CONTEXT."
|
||||
(let ((metadata (list :source-handle source-handle)))
|
||||
(dolist (field ebox-dsl--source-fields)
|
||||
(when-let* ((entry (ebox-dsl--optional-field
|
||||
properties field context)))
|
||||
(setq metadata
|
||||
(append metadata (list field (cdr entry))))))
|
||||
metadata))
|
||||
|
||||
(defun ebox-dsl--compile-style (tag properties context)
|
||||
"Compile and validate TAG author PROPERTIES for CONTEXT."
|
||||
(condition-case err
|
||||
(if properties
|
||||
(ebox-style-compile-form tag properties)
|
||||
nil)
|
||||
(error
|
||||
(error "%s rejected properties: %s"
|
||||
context (error-message-string err)))))
|
||||
|
||||
(defun ebox-dsl--layout-config
|
||||
(tag declarations extensions context)
|
||||
"Construct TAG LayoutConfig from DECLARATIONS and EXTENSIONS for CONTEXT."
|
||||
(if (eq tag 'text)
|
||||
nil
|
||||
(condition-case err
|
||||
(ebox-layout-config-for-form
|
||||
tag
|
||||
(if (memq tag '(row column))
|
||||
extensions
|
||||
(let ((names (ebox-layout-config-property-names tag)))
|
||||
(ebox-style-declaration-properties
|
||||
declarations
|
||||
(lambda (property)
|
||||
(memq (plist-get property :name) names))))))
|
||||
(error
|
||||
(error "%s rejected layout config: %s"
|
||||
context (error-message-string err))))))
|
||||
|
||||
(defun ebox-dsl--normalize-properties (tag properties source-handle)
|
||||
"Project evaluated TAG PROPERTIES for opaque SOURCE-HANDLE.
|
||||
|
||||
TAG is `text', `box', `row', `column', `flex', or `grid'. The returned
|
||||
`ebox-dsl--properties' contains typed constructor inputs, source metadata,
|
||||
and canonical declarations. Child forms and text payloads are outside this
|
||||
function."
|
||||
(unless (memq tag (cons 'text ebox-dsl--box-tags))
|
||||
(error "Ebox author properties has unknown author tag: %S" tag))
|
||||
(let* ((context (format "Ebox %S" tag))
|
||||
(properties
|
||||
(ebox-dsl--validate-plist properties context)))
|
||||
(dolist (field '(:content :layout :display :ebox-type :ebox-content-node))
|
||||
(when (plist-member properties field)
|
||||
(error "%s does not accept private author property %S"
|
||||
context field)))
|
||||
(let* ((outer-field
|
||||
(and (not (eq tag 'text))
|
||||
(ebox-dsl--optional-field
|
||||
properties :outer context)))
|
||||
(outer (and (not (eq tag 'text))
|
||||
(if outer-field (cdr outer-field) 'block)))
|
||||
(extension-keys (and (memq tag '(row column))
|
||||
'(:item-gap :cross-align)))
|
||||
(extensions
|
||||
(ebox-dsl--keep-keys
|
||||
properties extension-keys))
|
||||
(style-properties
|
||||
(ebox-dsl--without-keys
|
||||
properties
|
||||
(append (unless (eq tag 'text) '(:outer))
|
||||
extension-keys
|
||||
ebox-dsl--source-fields)))
|
||||
(declarations
|
||||
(ebox-dsl--compile-style
|
||||
tag style-properties context)))
|
||||
(ebox-dsl--make-properties
|
||||
:tag tag
|
||||
:source-metadata
|
||||
(ebox-dsl--source-metadata
|
||||
properties source-handle context)
|
||||
:outer outer
|
||||
:layout
|
||||
(ebox-dsl--layout-config
|
||||
tag declarations extensions context)
|
||||
:declarations declarations))))
|
||||
|
||||
(defconst ebox-dsl--tags '(text box row column flex grid)
|
||||
"The complete Ebox author tag set.")
|
||||
|
||||
(defun ebox-dsl--unquote-value (value)
|
||||
"Unquote one static DSL property VALUE when it is quoted data."
|
||||
(if (and (consp value)
|
||||
(eq (car value) 'quote)
|
||||
@ -29,218 +168,91 @@
|
||||
(cadr value)
|
||||
value))
|
||||
|
||||
(defun ebox--dsl-unquote-properties (items)
|
||||
"Unquote static property values in top-level DSL ITEMS.
|
||||
Structural child forms remain untouched; only values immediately following
|
||||
keyword properties are normalized."
|
||||
(defun ebox-dsl--unquote-properties (items)
|
||||
"Unquote static property values in top-level DSL ITEMS."
|
||||
(let (result)
|
||||
(while items
|
||||
(let ((item (pop items)))
|
||||
(if (keywordp item)
|
||||
(progn
|
||||
(unless items
|
||||
(error "ebox-build: missing value for %S" item))
|
||||
(error "Ebox build: missing value for %S" item))
|
||||
(push item result)
|
||||
(push (ebox--dsl-unquote-value (pop items)) result))
|
||||
(push (ebox-dsl--unquote-value (pop items)) result))
|
||||
(push item result))))
|
||||
(nreverse result)))
|
||||
|
||||
(defun ebox-dsl--axis-value (props property default allowed)
|
||||
"Return PROPS PROPERTY or DEFAULT after validating it against ALLOWED."
|
||||
(let ((value (if (plist-member props property)
|
||||
(plist-get props property)
|
||||
default)))
|
||||
(unless (memq value allowed)
|
||||
(error "ebox-build: invalid %S value %S" property value))
|
||||
value))
|
||||
(defun ebox-dsl--split (items)
|
||||
"Return `(PROPERTIES . CHILDREN)' parsed from author ITEMS."
|
||||
(let (properties children)
|
||||
(while items
|
||||
(let ((item (pop items)))
|
||||
(if (keywordp item)
|
||||
(progn
|
||||
(unless items
|
||||
(error "Ebox build: missing value for %S" item))
|
||||
(setq properties
|
||||
(append properties (list item (pop items)))))
|
||||
(setq children (append children (list item))))))
|
||||
(cons properties children)))
|
||||
|
||||
(defun ebox-dsl--with-display (node outer layout)
|
||||
"Return a shallow copy of NODE with canonical OUTER and LAYOUT display."
|
||||
(let ((copy (copy-sequence node)))
|
||||
(plist-put copy :display
|
||||
(list outer (if (eq layout 'normal) 'flow layout)))
|
||||
copy))
|
||||
|
||||
(defun ebox--build-children-layout (children)
|
||||
"Build a vertical default layout from CHILDREN."
|
||||
(setq children (delq nil children))
|
||||
(cond
|
||||
((null children) nil)
|
||||
((null (cdr children)) (ebox-dsl-build (car children)))
|
||||
(t (apply #'ebox-column (mapcar #'ebox-dsl-build children)))))
|
||||
|
||||
(defun ebox--build-box-content (children &optional _props)
|
||||
"Return content or child layout represented by DSL CHILDREN.
|
||||
The first value is string content. The second value is a lazy child node."
|
||||
(setq children (delq nil children))
|
||||
(cond
|
||||
((null children)
|
||||
(cl-values nil nil))
|
||||
((cl-every #'stringp children)
|
||||
(cl-values (string-join children "\n") nil))
|
||||
(t
|
||||
(cl-values nil (ebox--build-children-layout children)))))
|
||||
|
||||
(defun ebox--build-box (items)
|
||||
"Build a box node from DSL ITEMS."
|
||||
(let* ((split (ebox--build-split-attrs items))
|
||||
(props (car split))
|
||||
(children (cdr split)))
|
||||
(cl-multiple-value-bind (content child-node)
|
||||
(ebox--build-box-content children props)
|
||||
(when (and (or content child-node) (plist-member props :content))
|
||||
(error "ebox-build: box cannot combine :content with child nodes"))
|
||||
(apply #'ebox-create
|
||||
(append (if child-node
|
||||
(ebox--preformatted-box-props props)
|
||||
props)
|
||||
(when content (list :content content))
|
||||
(when child-node
|
||||
(list :ebox-content-node child-node)))))))
|
||||
|
||||
(defun ebox--build-text (items)
|
||||
"Build one canonical TextNode from DSL ITEMS."
|
||||
(let* ((split (ebox--build-split-attrs items))
|
||||
(props (car split))
|
||||
(payloads (delq nil (cdr split))))
|
||||
(when props
|
||||
(error "Ebox build: Text author properties are not implemented in this slice"))
|
||||
(defun ebox-dsl--build-text (_source items)
|
||||
"Build one canonical TextNode from author ITEMS."
|
||||
(let* ((split (ebox-dsl--split items))
|
||||
(properties (car split))
|
||||
(payloads (cdr split)))
|
||||
(unless (= (length payloads) 1)
|
||||
(error "ebox-build: text requires exactly one string payload"))
|
||||
(error "Ebox text requires exactly one string payload"))
|
||||
(unless (stringp (car payloads))
|
||||
(error "ebox-build: text payload must be a string: %S" (car payloads)))
|
||||
(apply #'ebox-text-create
|
||||
(append props (list :value (car payloads))))))
|
||||
(error "Ebox text payload must be a string: %S" (car payloads)))
|
||||
(let* ((plan (ebox-dsl--normalize-properties
|
||||
'text properties (ebox-dsl--source-handle)))
|
||||
(node
|
||||
(apply #'ebox-text-create
|
||||
(append (list :value (car payloads))
|
||||
(ebox-dsl--properties-source-metadata plan)
|
||||
(list :declarations
|
||||
(ebox-dsl--properties-declarations plan))))))
|
||||
node)))
|
||||
|
||||
(defun ebox--build-wrap-layout (tag props layout)
|
||||
"Wrap LAYOUT with box PROPS for DSL TAG, or return LAYOUT unchanged."
|
||||
(if (null props)
|
||||
layout
|
||||
(when (plist-member props :content)
|
||||
(error "ebox-build: %S cannot combine :content with child nodes" tag))
|
||||
(apply #'ebox-create
|
||||
(append (ebox--preformatted-box-props props)
|
||||
(list :display '(block flow)
|
||||
:ebox-content-node layout)))))
|
||||
|
||||
(defun ebox--build-layout (tag constructor items)
|
||||
"Build a row/column layout TAG with CONSTRUCTOR from DSL ITEMS."
|
||||
(let* ((split (ebox--build-split-attrs items))
|
||||
(props (car split))
|
||||
(children (cdr split))
|
||||
(layout
|
||||
(apply constructor
|
||||
(mapcar (lambda (child)
|
||||
(if (ebox-child-range--descriptor-p child)
|
||||
child
|
||||
(ebox-dsl-build child)))
|
||||
(delq nil children)))))
|
||||
(ebox--build-wrap-layout tag props layout)))
|
||||
|
||||
(defun ebox--build-flex (items)
|
||||
"Build a flex node from DSL ITEMS."
|
||||
(let* ((split (ebox--build-split-attrs items))
|
||||
(props (car split))
|
||||
(children (mapcar #'ebox-dsl-build (delq nil (cdr split)))))
|
||||
(apply #'ebox-flex (append props children))))
|
||||
|
||||
(defun ebox--build-flex-item (items)
|
||||
"Build a flex item wrapper from DSL ITEMS."
|
||||
(let* ((split (ebox--build-split-attrs items))
|
||||
(props (car split))
|
||||
(children (cdr split))
|
||||
(item-props (ebox--plist-keep-keys props ebox--flex-item-prop-keys))
|
||||
(box-props (ebox--plist-remove-keys props ebox--flex-item-prop-keys)))
|
||||
(unless children
|
||||
(error "ebox-build: item requires a child node"))
|
||||
(let ((child (ebox--flex-wrap-node-with-box-props
|
||||
'item (ebox--build-children-layout children) box-props)))
|
||||
(dolist (key ebox--flex-item-prop-keys)
|
||||
(when (plist-member item-props key)
|
||||
(plist-put child key (plist-get item-props key))))
|
||||
(apply #'ebox-flex-item child item-props))))
|
||||
|
||||
(defun ebox--build-grid (items)
|
||||
"Build a grid node from DSL ITEMS."
|
||||
(let* ((split (ebox-grid--split-attrs items))
|
||||
(props (car split))
|
||||
(children (mapcar #'ebox-dsl-build (delq nil (cdr split)))))
|
||||
(apply #'ebox-grid (append props children))))
|
||||
|
||||
(defun ebox--build-spacer (items)
|
||||
"Build a spacer node from DSL ITEMS."
|
||||
(let* ((split (ebox--build-split-attrs items))
|
||||
(props (car split))
|
||||
(children (cdr split)))
|
||||
(when children
|
||||
(error "ebox-build: spacer cannot have child nodes"))
|
||||
(when (plist-member props :content)
|
||||
(error "ebox-build: spacer cannot set :content"))
|
||||
(apply #'ebox-spacer props)))
|
||||
|
||||
(defun ebox--build-canonical-box (items)
|
||||
"Build canonical Box ITEMS with orthogonal outer and layout axes."
|
||||
(let* ((split (ebox--build-split-attrs items))
|
||||
(props (car split))
|
||||
(children (cdr split))
|
||||
(outer (ebox-dsl--axis-value
|
||||
props :outer 'block ebox-dsl--outer-values))
|
||||
(layout (ebox-dsl--axis-value
|
||||
props :layout 'normal ebox-dsl--layout-values))
|
||||
(box-props (ebox--plist-remove-keys props '(:outer :layout)))
|
||||
node)
|
||||
(setq node
|
||||
(pcase layout
|
||||
('normal
|
||||
(ebox--build-box (append box-props children)))
|
||||
('row
|
||||
(ebox--build-layout
|
||||
'box #'ebox-row (append box-props children)))
|
||||
('column
|
||||
(ebox--build-layout
|
||||
'box #'ebox-column (append box-props children)))
|
||||
('flex
|
||||
(ebox--build-flex (append box-props children)))
|
||||
('grid
|
||||
(ebox--build-grid (append box-props children)))))
|
||||
(ebox-dsl--with-display node outer layout)))
|
||||
(defun ebox-dsl--build-box (tag _source items)
|
||||
"Build canonical BoxNode TAG from author ITEMS."
|
||||
(let* ((split (ebox-dsl--split items))
|
||||
(properties (car split))
|
||||
(child-forms (cdr split))
|
||||
(plan (ebox-dsl--normalize-properties
|
||||
tag properties (ebox-dsl--source-handle)))
|
||||
(children (mapcar #'ebox-dsl-build child-forms))
|
||||
(node
|
||||
(apply #'ebox-box-create
|
||||
(append
|
||||
(list :layout (ebox-dsl--properties-layout plan)
|
||||
:outer (ebox-dsl--properties-outer plan)
|
||||
:children children
|
||||
:declarations
|
||||
(ebox-dsl--properties-declarations plan))
|
||||
(ebox-dsl--properties-source-metadata plan)))))
|
||||
node))
|
||||
|
||||
(defun ebox-dsl-build (dsl)
|
||||
"Build an Ebox node from an ETML-style list DSL.
|
||||
"Build one canonical Ebox node from inert author DSL.
|
||||
|
||||
Supported forms:
|
||||
(box :content \"Hello\" :width (160))
|
||||
(box :content \"Fits preview\" :width (viewport))
|
||||
(box :width (240) (row (box \"A\") (box \"B\")))
|
||||
(row (box :content \"Left\") (spacer :width (16)) (box :content \"Right\"))
|
||||
(column (box :content \"Top\") (box :content \"Bottom\"))
|
||||
(grid :grid-template-columns ((80) (80))
|
||||
(box :grid-column (1 :span 2) \"Header\"))
|
||||
(flex :width (320) :gap (1 (12)) (item :flex 1 (box \"A\")))
|
||||
|
||||
`box' and `ebox' create `ebox-create' nodes. `row', `column',
|
||||
`flex', and `spacer' compile to existing Ebox layout helpers. `row'
|
||||
and `column' may also receive box properties; those properties wrap
|
||||
the child layout in a box using the rendered child layout as content."
|
||||
The complete grammar is String plus `text', `box', `row', `column', `flex',
|
||||
and `grid'. Strings normalize to Text. The five Box forms select a typed
|
||||
LayoutConfig; no form accepts `:content', `:layout', or a raw runtime node."
|
||||
(cond
|
||||
((ebox--node-p dsl) dsl)
|
||||
((stringp dsl) (ebox-text-create :value dsl))
|
||||
((stringp dsl)
|
||||
(ebox-dsl--build-text dsl (list dsl)))
|
||||
((not (and (consp dsl) (symbolp (car dsl))))
|
||||
(error "ebox-build: invalid DSL node %S" dsl))
|
||||
(error "Ebox build: invalid author node %S" dsl))
|
||||
((not (memq (car dsl) ebox-dsl--tags))
|
||||
(error "Ebox build: unknown author tag %S" (car dsl)))
|
||||
((eq (car dsl) 'text)
|
||||
(ebox-dsl--build-text
|
||||
dsl (ebox-dsl--unquote-properties (cdr dsl))))
|
||||
(t
|
||||
(let ((tag (car dsl))
|
||||
(items (ebox--dsl-unquote-properties (cdr dsl))))
|
||||
(pcase tag
|
||||
('text (ebox--build-text items))
|
||||
('box (ebox--build-canonical-box items))
|
||||
('ebox (ebox--build-box items))
|
||||
('row (ebox--build-layout tag #'ebox-row items))
|
||||
('column (ebox--build-layout tag #'ebox-column items))
|
||||
('flex (ebox--build-flex items))
|
||||
('item (ebox--build-flex-item items))
|
||||
('spacer (ebox--build-spacer items))
|
||||
('grid (ebox--build-grid items))
|
||||
(_ (error "ebox-build: unknown DSL tag %S" tag)))))))
|
||||
(ebox-dsl--build-box
|
||||
(car dsl) dsl (ebox-dsl--unquote-properties (cdr dsl))))))
|
||||
|
||||
(provide 'ebox-dsl)
|
||||
|
||||
|
||||
247
ebox-flex.el
247
ebox-flex.el
@ -40,9 +40,8 @@
|
||||
(cache-key signature rendered &optional side-effects))
|
||||
(declare-function ebox--render-with-cache
|
||||
"ebox-incremental" (node &optional force cache-probe))
|
||||
(declare-function ebox-tree--register-legacy-flex-item-adapter
|
||||
"ebox-tree" (adapter token source content))
|
||||
(declare-function ebox-create "ebox" (&rest plist))
|
||||
(declare-function ebox-node-factory--create-from-properties
|
||||
"ebox-node-factory" (&rest plist))
|
||||
(declare-function ebox-get "ebox" (box property))
|
||||
(declare-function ebox--ensure-node-id "ebox" (node))
|
||||
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||
@ -82,14 +81,6 @@
|
||||
'(:order :flex :flex-grow :flex-shrink :flex-basis :align-self)
|
||||
"Properties that belong to flex item metadata.")
|
||||
|
||||
(defconst ebox--flex-item-direct-adapter-token
|
||||
(make-symbol "ebox-flex-item-direct-adapter")
|
||||
"Opaque provenance token for a legacy Flex item without a Box wrapper.")
|
||||
|
||||
(defconst ebox--flex-item-wrapper-adapter-token
|
||||
(make-symbol "ebox-flex-item-wrapper-adapter")
|
||||
"Opaque provenance token for a legacy Flex item with one Box wrapper.")
|
||||
|
||||
(defconst ebox--flex-container-layout-prop-keys
|
||||
'(:flex-direction :flex-wrap :flex-flow
|
||||
:justify-content :align-items :align-content
|
||||
@ -176,28 +167,12 @@ That text is preformatted and must not be sent through the default wrapper."
|
||||
props
|
||||
(append props (list :wrap-mode nil))))
|
||||
|
||||
(defun ebox--flex-wrap-node-with-box-props (tag node props)
|
||||
"Wrap NODE in an `ebox-create' box using PROPS for DSL/API TAG."
|
||||
(if (null props)
|
||||
node
|
||||
(when (plist-member props :content)
|
||||
(error "ebox-build: %S cannot combine :content with child nodes" tag))
|
||||
(let ((box (apply #'ebox-create
|
||||
(append (ebox--preformatted-box-props props)
|
||||
(list :display '(block flow)
|
||||
:ebox-content-node node)))))
|
||||
(when (listp node)
|
||||
(dolist (key ebox--flex-item-prop-keys)
|
||||
(when (plist-member node key)
|
||||
(plist-put box key (plist-get node key)))))
|
||||
box)))
|
||||
|
||||
(defun ebox--flex-make-wrapper-box (props)
|
||||
"Return a flex container wrapper box for PROPS, or nil."
|
||||
(when (and props (ebox--flex-visual-box-props-p props))
|
||||
(when (plist-member props :content)
|
||||
(error "ebox-flex: flex cannot combine :content with child nodes"))
|
||||
(apply #'ebox-create
|
||||
(apply #'ebox-node-factory--create-from-properties
|
||||
(append (ebox--preformatted-box-props props)
|
||||
(list :content "")))))
|
||||
|
||||
@ -528,13 +503,22 @@ programmatic port and are therefore rejected here."
|
||||
(plist-get source :ebox-type)
|
||||
(not (eq (plist-get source :ebox-type) 'box))))
|
||||
|
||||
(defun ebox--flex-bind-source-viewport (source viewport thunk)
|
||||
(defun ebox--flex-box-source-p (source)
|
||||
"Return non-nil when SOURCE owns Box geometry for Flex sizing."
|
||||
(and (listp source)
|
||||
(not (stringp source))
|
||||
(eq (plist-get source :ebox-type) 'box)
|
||||
(not (eq (plist-get source :ebox-kind) 'text))))
|
||||
|
||||
(defun ebox--flex-bind-source-viewport (_source viewport thunk)
|
||||
"Call THUNK in SOURCE's detached Flex-item render context.
|
||||
VIEWPORT is bound only for composite SOURCE nodes; the Flex parent context is
|
||||
always bound because SOURCE remains a direct Flex participant while rendered
|
||||
as a detached geometry root."
|
||||
When VIEWPORT is non-nil it is the containing block assigned by the Flex
|
||||
parent and therefore applies to SOURCE's complete subtree, including a
|
||||
canonical Box with responsive descendants. Intrinsic measurement passes nil
|
||||
explicitly. The Flex parent context is always bound because SOURCE remains a
|
||||
direct Flex participant while rendered as a detached geometry root."
|
||||
(let ((ebox--render-root-parent-kind 'flex))
|
||||
(if (and viewport (ebox--flex-composite-source-p source))
|
||||
(if viewport
|
||||
(let ((ebox-viewport-width viewport))
|
||||
(funcall thunk))
|
||||
(funcall thunk))))
|
||||
@ -555,7 +539,8 @@ width cannot affect that final output. The first occurrence is only observed;
|
||||
this avoids signature and retained-cost work for one-off responsive sizes."
|
||||
(when (and ebox--render-cache-table
|
||||
(numberp viewport)
|
||||
(ebox--flex-composite-source-p source))
|
||||
(or (ebox--flex-composite-source-p source)
|
||||
(ebox--viewport-dependent-subtree-p source)))
|
||||
(let* ((observations
|
||||
(or (gethash ebox--render-cache-table
|
||||
ebox--flex-sized-render-observation-table)
|
||||
@ -589,7 +574,7 @@ this avoids signature and retained-cost work for one-off responsive sizes."
|
||||
Direct box flex items are measured intrinsically. Composite items receive
|
||||
VIEWPORT for descendants so auto and max-width layouts can resolve against
|
||||
their containing block, while stack/column containers do not stretch their own
|
||||
outer measurement width to VIEWPORT."
|
||||
outer measurement width to VIEWPORT."
|
||||
(if (ebox--flex-composite-source-p source)
|
||||
(ebox--flex-bind-source-viewport
|
||||
source viewport
|
||||
@ -618,7 +603,7 @@ grapheme."
|
||||
(let ((declared-min (or (ebox--resolve-size-content-pixel
|
||||
box (ebox-get box :min-width) 0)
|
||||
0)))
|
||||
(if (null (ebox-get box :wrap-mode))
|
||||
(if (ebox-style-no-soft-wrap-p (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
|
||||
@ -660,13 +645,13 @@ grapheme."
|
||||
|
||||
(defun ebox--flex-min-main (source rendered axis)
|
||||
"Return SOURCE's minimum main-axis size for flex layout on AXIS."
|
||||
(if (and (listp source) (eq (plist-get source :ebox-type) 'box))
|
||||
(if (ebox--flex-box-source-p source)
|
||||
(ebox--flex-box-min-main source rendered axis)
|
||||
(ebox--flex-string-main rendered axis)))
|
||||
|
||||
(defun ebox--flex-max-main (source axis)
|
||||
"Return SOURCE's maximum main-axis size for flex layout on AXIS."
|
||||
(when (and (listp source) (eq (plist-get source :ebox-type) 'box))
|
||||
(when (ebox--flex-box-source-p source)
|
||||
(ebox--flex-box-main-constraint
|
||||
source axis (if (eq axis 'row) :max-width :max-height))))
|
||||
|
||||
@ -683,12 +668,11 @@ grapheme."
|
||||
((eq basis 'auto)
|
||||
(ebox--flex-string-main rendered axis))
|
||||
((eq basis 'content)
|
||||
(if (and (listp source) (eq (plist-get source :ebox-type) 'box))
|
||||
(if (ebox--flex-box-source-p source)
|
||||
(ebox--flex-box-content-basis-main source rendered axis)
|
||||
(ebox--flex-string-main rendered axis)))
|
||||
((and (eq axis 'row)
|
||||
(listp source)
|
||||
(eq (plist-get source :ebox-type) 'box))
|
||||
(ebox--flex-box-source-p source))
|
||||
(let* ((normalized
|
||||
(ebox--normalize-horizontal-size-value
|
||||
basis ebox--preferred-size-keywords))
|
||||
@ -727,9 +711,7 @@ grapheme."
|
||||
(ebox--flex-recache-source-boxes child)))
|
||||
('flex
|
||||
(dolist (child (ebox-tree-layout-children node))
|
||||
(ebox--flex-recache-source-boxes child)))
|
||||
('flex-item
|
||||
(ebox--flex-recache-source-boxes (plist-get node :node)))))))
|
||||
(ebox--flex-recache-source-boxes child)))))))
|
||||
|
||||
(defun ebox--flex-box-main-margin (box axis)
|
||||
"Return BOX's total margin on AXIS."
|
||||
@ -763,7 +745,7 @@ grapheme."
|
||||
|
||||
(defun ebox--flex-copy-node-for-size (node axis main cross stretch)
|
||||
"Return NODE or a copy with flex-computed MAIN/CROSS size."
|
||||
(if (and (listp node) (eq (plist-get node :ebox-type) 'box))
|
||||
(if (ebox--flex-box-source-p node)
|
||||
(let ((copy (copy-sequence node)))
|
||||
(ebox--ensure-region-id node)
|
||||
(plist-put copy :region-id (ebox-get node :region-id))
|
||||
@ -784,7 +766,11 @@ grapheme."
|
||||
|
||||
(defun ebox--flex-measure-cacheable-p (source props)
|
||||
"Return non-nil when SOURCE and PROPS have stable flex measurements."
|
||||
(and (ebox--render-cacheable-node-p source)
|
||||
(and (or (and (eq (and (listp source)
|
||||
(plist-get source :ebox-type))
|
||||
'box)
|
||||
(not (ebox--flex-composite-source-p source)))
|
||||
(ebox--render-cacheable-node-p source))
|
||||
(not (ebox--viewport-dependent-size-value-p
|
||||
(plist-get props :flex-basis)))))
|
||||
|
||||
@ -793,13 +779,20 @@ grapheme."
|
||||
The result is nil when SOURCE cannot use the persistent measurement cache;
|
||||
otherwise it carries the key, signature, and any cached measurement so a
|
||||
caller can pass the same miss context to the renderer."
|
||||
(when (ebox--flex-measure-cacheable-p source props)
|
||||
(let* ((cache-key
|
||||
(list (ebox--ensure-node-id source) 'flex-measure axis props))
|
||||
(signature (ebox--flex-render-cache-signature source)))
|
||||
(list :cache-key cache-key
|
||||
:signature signature
|
||||
:cached (ebox--render-cache-lookup cache-key signature)))))
|
||||
(let ((direct-box-p
|
||||
(and (eq (and (listp source) (plist-get source :ebox-type)) 'box)
|
||||
(not (ebox--flex-composite-source-p source)))))
|
||||
;; `ebox--flex-render-source-for-measurement' renders a direct Box with no
|
||||
;; ambient inline viewport. Compute the cache proof in that exact same
|
||||
;; context so a root resize cannot invalidate an unchanged natural size.
|
||||
(let ((ebox-viewport-width (if direct-box-p nil ebox-viewport-width)))
|
||||
(when (ebox--flex-measure-cacheable-p source props)
|
||||
(let* ((cache-key
|
||||
(list (ebox--ensure-node-id source) 'flex-measure axis props))
|
||||
(signature (ebox--flex-render-cache-signature source)))
|
||||
(list :cache-key cache-key
|
||||
:signature signature
|
||||
:cached (ebox--render-cache-lookup cache-key signature)))))))
|
||||
|
||||
(defun ebox--flex-measure-cache-value (probe)
|
||||
"Return a copied cached measurement from PROBE, or nil on a miss."
|
||||
@ -871,11 +864,11 @@ box heights use declared line constraints. In those cases the final sized
|
||||
render can be the first render instead of rendering the whole subtree once for
|
||||
measurement and immediately rendering it again at the computed size."
|
||||
(let ((basis (plist-get props :flex-basis)))
|
||||
(when (and (listp source)
|
||||
(eq (plist-get source :ebox-type) 'box)
|
||||
(when (and (ebox--flex-box-source-p source)
|
||||
(ebox--flex-fixed-basis-value-p basis)
|
||||
(or (not (eq axis 'row))
|
||||
(ebox-get source :wrap-mode)))
|
||||
(ebox-style-soft-wrap-p
|
||||
(ebox-get source :wrap-mode))))
|
||||
(let* ((min-main (ebox--flex-min-main source "" axis))
|
||||
(max-main (ebox--flex-max-main source axis))
|
||||
(base (ebox--flex-basis-main source "" axis basis))
|
||||
@ -1013,10 +1006,7 @@ Each item in PARTS is a cons cell (RENDERED . WIDTH)."
|
||||
"Render flex ITEM with MAIN and optional CROSS size.
|
||||
Return a sized render entry containing the rendered string and dimensions."
|
||||
(let* ((source (plist-get item :source))
|
||||
(source-box-p
|
||||
(eq (and (listp source)
|
||||
(plist-get source :ebox-type))
|
||||
'box))
|
||||
(source-box-p (ebox--flex-box-source-p source))
|
||||
(align (ebox--flex-resolved-align item container-align))
|
||||
(stretch (memq align '(stretch normal)))
|
||||
(render-main (plist-get item :render-main))
|
||||
@ -1265,7 +1255,7 @@ Return a sized render entry containing the rendered string and dimensions."
|
||||
(let ((items
|
||||
(cl-loop for child in children
|
||||
for index from 0
|
||||
for source = (ebox--flex-item-source-node child)
|
||||
for source = child
|
||||
for props = (ebox--flex-item-props child)
|
||||
for measurement = (ebox--flex-item-measurement
|
||||
source props axis viewport)
|
||||
@ -1296,7 +1286,7 @@ Return a sized render entry containing the rendered string and dimensions."
|
||||
(puthash source-id t source-set))
|
||||
(cl-loop for child in children
|
||||
for index from 0
|
||||
for source = (ebox--flex-item-source-node child)
|
||||
for source = child
|
||||
for source-id = (plist-get source :node-id)
|
||||
when (gethash source-id source-set)
|
||||
do (let* ((props (ebox--flex-item-props child))
|
||||
@ -1640,65 +1630,6 @@ metadata and rendering decisions."
|
||||
(ebox--flex-pad-width result cross-size)
|
||||
result))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-flex-item (node &rest plist)
|
||||
"Wrap NODE with flex item metadata and optional box properties from PLIST."
|
||||
(let ((item-props (ebox--plist-keep-keys plist ebox--flex-item-prop-keys))
|
||||
(box-props (ebox--plist-remove-keys plist ebox--flex-item-prop-keys)))
|
||||
(let* ((source (ebox--flex-wrap-node-with-box-props
|
||||
'ebox-flex-item node box-props))
|
||||
(source (copy-sequence source))
|
||||
(declarations
|
||||
(append (plist-get source :ebox-style-declarations)
|
||||
(ebox-style-compile-declarations item-props)))
|
||||
(adapter (list :ebox-type 'flex-item
|
||||
:display '(block flow)
|
||||
:node source
|
||||
:props item-props))
|
||||
(token (if box-props
|
||||
ebox--flex-item-wrapper-adapter-token
|
||||
ebox--flex-item-direct-adapter-token)))
|
||||
(plist-put source :ebox-style-declarations declarations)
|
||||
(ebox-tree--register-legacy-flex-item-adapter
|
||||
adapter token source
|
||||
(and box-props (plist-get source :ebox-content-node))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-flex (&rest items)
|
||||
"Return a lazy flex layout node from ITEMS.
|
||||
|
||||
Container properties include flex layout properties
|
||||
\(:flex-direction, :flex-wrap, :flex-flow, :justify-content, :align-items,
|
||||
:align-content, :gap, :row-gap, :column-gap) plus normal `ebox-create'
|
||||
box properties. When visual box properties are present, :width and :height
|
||||
size the outer box and the inner flex layout uses its content area.
|
||||
|
||||
Item properties can be stored with `ebox-flex-item' or directly on a
|
||||
child box: :order, :flex-grow, :flex-shrink, :flex-basis, :flex, and
|
||||
:align-self. `ebox-flex-item' can also carry normal box properties around
|
||||
its child."
|
||||
(let* ((split (ebox--build-split-attrs items))
|
||||
(raw-props (car split))
|
||||
(box-props (ebox--plist-remove-keys
|
||||
raw-props ebox--flex-container-layout-prop-keys))
|
||||
(wrapper-box (ebox--flex-make-wrapper-box box-props))
|
||||
(props (ebox--flex-normalize-container-props raw-props))
|
||||
(children (delq nil (cdr split)))
|
||||
(node (list :ebox-type 'flex
|
||||
:display '(block flex)
|
||||
:props props
|
||||
:raw-props raw-props
|
||||
:ebox-style-declarations
|
||||
(ebox-style-compile-declarations raw-props)
|
||||
:box wrapper-box
|
||||
:children children)))
|
||||
(when wrapper-box
|
||||
(plist-put wrapper-box :ebox-style-wrapper t))
|
||||
(dolist (key '(:id :class :selector-state :selector-attributes :key))
|
||||
(when (plist-member raw-props key)
|
||||
(plist-put node key (plist-get raw-props key))))
|
||||
node))
|
||||
|
||||
(defun ebox--render-flex-box (node rendered)
|
||||
"Render flex NODE's container box around RENDERED, when present."
|
||||
(if-let* ((box (plist-get node :box)))
|
||||
@ -1825,7 +1756,7 @@ assembles already-rendered items."
|
||||
(cond
|
||||
((< measure-index (length children))
|
||||
(let* ((child (nth measure-index children))
|
||||
(source (ebox--flex-item-source-node child))
|
||||
(source child)
|
||||
(props (ebox--flex-item-props child))
|
||||
(measurement
|
||||
(ebox--flex-item-measurement
|
||||
@ -1856,6 +1787,29 @@ assembles already-rendered items."
|
||||
(plist-put entry :render-index (1+ render-index))
|
||||
t))))))))
|
||||
|
||||
(defun ebox--flex-window-canonical-node-p (node)
|
||||
"Return non-nil when NODE is one canonical Flex Layout Box."
|
||||
(and (eq (plist-get node :ebox-kind) 'box)
|
||||
(when-let* ((config (plist-get node :ebox-layout-config)))
|
||||
(eq (ebox-layout-config-kind config) 'flex))))
|
||||
|
||||
(defun ebox--flex-window-box (node)
|
||||
"Return NODE's Box wrapper for lazy Flex rendering."
|
||||
(if (ebox--flex-window-canonical-node-p node)
|
||||
node
|
||||
(plist-get node :box)))
|
||||
|
||||
(defun ebox--flex-window-props (node)
|
||||
"Return NODE's resolved Flex properties for lazy row production."
|
||||
(if (ebox--flex-window-canonical-node-p node)
|
||||
(append
|
||||
(ebox-layout-config-props (plist-get node :ebox-layout-config))
|
||||
(ebox--flex-box-constraints node))
|
||||
(ebox--flex-container-content-props
|
||||
(plist-get node :props)
|
||||
(plist-get node :raw-props)
|
||||
(plist-get node :box))))
|
||||
|
||||
(defun ebox--flex-window-wrapper-supported-p (box)
|
||||
"Return non-nil when BOX can safely wrap an incomplete flex prefix."
|
||||
(or (null box)
|
||||
@ -1863,7 +1817,8 @@ assembles already-rendered items."
|
||||
(or (null (ebox-get box :min-height))
|
||||
(equal (ebox-get box :min-height) 0))
|
||||
(null (ebox-get box :max-height))
|
||||
(null (ebox-get box :wrap-mode))
|
||||
(or (ebox-style-no-soft-wrap-p (ebox-get box :wrap-mode))
|
||||
(ebox--flex-window-canonical-node-p box))
|
||||
(eq (ebox-get box :text-align) 'left)
|
||||
(eq (ebox-get box :vertical-align) 'top))))
|
||||
|
||||
@ -1872,6 +1827,16 @@ assembles already-rendered items."
|
||||
When COMPLETE is nil, trailing vertical chrome is suppressed so the result is
|
||||
a prefix of the eventual full wrapper render."
|
||||
(let ((copy (copy-sequence box)))
|
||||
(when (ebox--flex-window-canonical-node-p copy)
|
||||
;; The lazy producer has already rendered the Flex children. Lower the
|
||||
;; private wrapper copy to Normal content so the generic Box painter
|
||||
;; applies only this Box's chrome and ownership once.
|
||||
(cl-remf copy :children)
|
||||
(cl-remf copy :ebox-child-sequence)
|
||||
(plist-put copy :ebox-layout-config (ebox-normal-layout-create))
|
||||
(plist-put copy :display
|
||||
(list (or (car-safe (plist-get copy :display)) 'block)
|
||||
'flow)))
|
||||
(unless complete
|
||||
(plist-put copy :padding-bottom-height 0)
|
||||
(plist-put copy :margin-bottom-height 0)
|
||||
@ -1912,7 +1877,7 @@ a prefix of the eventual full wrapper render."
|
||||
(defun ebox--flex-window-render-box-lines (node content complete limit)
|
||||
"Return NODE flex wrapper lines around CONTENT up to LIMIT."
|
||||
(let* ((rendered
|
||||
(if-let* ((box (plist-get node :box)))
|
||||
(if-let* ((box (ebox--flex-window-box node)))
|
||||
(let ((copy (ebox--flex-window-wrapper-copy box complete))
|
||||
(region-id (ebox--ensure-region-id box)))
|
||||
(plist-put copy :region-id region-id)
|
||||
@ -1952,7 +1917,8 @@ the chunk begins at the wrapper top and should include top chrome."
|
||||
(border-top-p (and include-top
|
||||
(ebox-get box :border-top-p)))
|
||||
(border-top-color (ebox-get box :border-top-color))
|
||||
(color (ebox-get box :color))
|
||||
(color (and (ebox-style--text-paint-owner-p box)
|
||||
(ebox-get box :color)))
|
||||
(bgcolor (ebox-get box :bgcolor))
|
||||
(inner-height (+ (length content-lines) padding-top))
|
||||
result)
|
||||
@ -2060,14 +2026,13 @@ the chunk begins at the wrapper top and should include top chrome."
|
||||
(defun ebox--flex-window-definite-main (source axis)
|
||||
"Return SOURCE's definite main size for prefix flex rendering."
|
||||
(when (and (eq axis 'row)
|
||||
(listp source)
|
||||
(eq (plist-get source :ebox-type) 'box)
|
||||
(ebox--flex-box-source-p source)
|
||||
(plist-member source :width))
|
||||
(ebox--flex-box-main-constraint source axis :width)))
|
||||
|
||||
(defun ebox--flex-window-cheap-item (child axis index)
|
||||
"Return cheap flex item metadata for CHILD, or nil when unsupported."
|
||||
(let* ((source (ebox--flex-item-source-node child))
|
||||
(let* ((source child)
|
||||
(props (ebox--flex-item-props child))
|
||||
(basis (plist-get props :flex-basis))
|
||||
(main (ebox--flex-window-definite-main source axis))
|
||||
@ -2096,7 +2061,8 @@ the chunk begins at the wrapper top and should include top chrome."
|
||||
:base main
|
||||
:target main
|
||||
:window-frozen-shrinkable
|
||||
(and (ebox-get source :wrap-mode)
|
||||
(and (ebox-style-soft-wrap-p
|
||||
(ebox-get source :wrap-mode))
|
||||
(not (zerop (or (plist-get props :flex-shrink) 1)))
|
||||
t))))))
|
||||
|
||||
@ -2109,7 +2075,8 @@ the chunk begins at the wrapper top and should include top chrome."
|
||||
(memq (plist-get props :flex-wrap) '(wrap nowrap))
|
||||
(not (ebox--flex-reverse-p props))
|
||||
(ebox--flex-default-order-p children)
|
||||
(ebox--flex-window-wrapper-supported-p (plist-get node :box))))
|
||||
(ebox--flex-window-wrapper-supported-p
|
||||
(ebox--flex-window-box node))))
|
||||
|
||||
(defun ebox--flex-window-lines-from-pieces (pieces)
|
||||
"Return flex content string from already rendered row PIECES."
|
||||
@ -2126,7 +2093,7 @@ the chunk begins at the wrapper top and should include top chrome."
|
||||
|
||||
(defun ebox--flex-window-prefix-line-count (node pieces)
|
||||
"Return visible line count for NODE prefix PIECES without wrapper rerender."
|
||||
(+ (ebox--flex-window-top-chrome-lines (plist-get node :box))
|
||||
(+ (ebox--flex-window-top-chrome-lines (ebox--flex-window-box node))
|
||||
(if pieces
|
||||
(ebox-string-height
|
||||
(ebox--flex-window-lines-from-pieces pieces))
|
||||
@ -2269,7 +2236,7 @@ the chunk begins at the wrapper top and should include top chrome."
|
||||
|
||||
(defun ebox--flex-window-completion-prefix-tail-rewrite-count (node)
|
||||
"Return how many cached prefix tail lines completion rewrites for NODE."
|
||||
(let ((box (plist-get node :box)))
|
||||
(let ((box (ebox--flex-window-box node)))
|
||||
(if (and box
|
||||
(ebox-get box :border-bottom-p)
|
||||
(= (floor (ebox-get box :padding-bottom-height)) 0))
|
||||
@ -2320,7 +2287,8 @@ the chunk begins at the wrapper top and should include top chrome."
|
||||
(+ cached-content-count (length new-content-lines))
|
||||
(length (ebox-string-lines content))))
|
||||
(prefix-line-count
|
||||
(+ (ebox--flex-window-top-chrome-lines (plist-get node :box))
|
||||
(+ (ebox--flex-window-top-chrome-lines
|
||||
(ebox--flex-window-box node))
|
||||
prospective-content-count))
|
||||
(source-complete-visible-p
|
||||
(and source-complete
|
||||
@ -2331,14 +2299,14 @@ the chunk begins at the wrapper top and should include top chrome."
|
||||
(wrapper-lines
|
||||
(and chunk-wrapper-p new-content-lines
|
||||
(ebox--flex-window-render-wrapper-chunk-lines
|
||||
(plist-get node :box)
|
||||
(ebox--flex-window-box node)
|
||||
new-content-lines cached-content-count seedless-chunk-p)))
|
||||
(prospective-line-count
|
||||
(+ cached-line-count (length wrapper-lines)))
|
||||
(completion-prefix-supported-p
|
||||
(and chunk-wrapper-p effective-source-complete
|
||||
(ebox--flex-window-complete-prefix-supported-p
|
||||
(plist-get node :box))))
|
||||
(ebox--flex-window-box node))))
|
||||
(exact-append-p
|
||||
(or completion-prefix-supported-p
|
||||
(and chunk-wrapper-p (not effective-source-complete))))
|
||||
@ -2357,7 +2325,7 @@ the chunk begins at the wrapper top and should include top chrome."
|
||||
(completed-tail-line
|
||||
(and completion-prefix-supported-p prospective-tail
|
||||
(ebox--flex-window-complete-tail-line
|
||||
(plist-get node :box) (car prospective-tail))))
|
||||
(ebox--flex-window-box node) (car prospective-tail))))
|
||||
content-lines
|
||||
content-line-count
|
||||
content-line-tail
|
||||
@ -2454,10 +2422,7 @@ Return nil when the flex container needs the normal full renderer."
|
||||
(ebox--flex-window-cache-store
|
||||
node limit
|
||||
(catch 'unsupported
|
||||
(let* ((props (ebox--flex-container-content-props
|
||||
(plist-get node :props)
|
||||
(plist-get node :raw-props)
|
||||
(plist-get node :box)))
|
||||
(let* ((props (ebox--flex-window-props node))
|
||||
(axis (ebox--flex-axis props))
|
||||
(children (ebox-tree-layout-children node))
|
||||
(main-size (if (eq axis 'row)
|
||||
@ -2493,7 +2458,7 @@ Return nil when the flex container needs the normal full renderer."
|
||||
(visible-line-count
|
||||
(or (plist-get seed :prefix-line-count)
|
||||
(ebox--flex-window-top-chrome-lines
|
||||
(plist-get node :box))))
|
||||
(ebox--flex-window-box node))))
|
||||
result
|
||||
complete)
|
||||
(cl-labels
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
|
||||
(declare-function ebox--current-display-signature "ebox-measure" ())
|
||||
(declare-function ebox--ensure-node-id "ebox" (node))
|
||||
(declare-function ebox-style--declaration-value-equal-p
|
||||
"ebox-style" (left right))
|
||||
(declare-function ebox--complete-layout-snapshot
|
||||
"ebox-incremental"
|
||||
(buffer node snapshot &optional copy))
|
||||
@ -129,18 +131,24 @@ Backend marker and buffer span data are intentionally excluded."
|
||||
:border-top-color :border-bottom-color)
|
||||
"Box properties that affect paint without changing layout geometry.")
|
||||
|
||||
(defconst ebox--typography-style-signature-keys
|
||||
'(:font :font-family :font-height :font-weight :font-slant)
|
||||
"Typography properties whose resolved metrics may change 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)
|
||||
(append
|
||||
'(:box-sizing
|
||||
:width :min-width :max-width
|
||||
:height :min-height :max-height)
|
||||
ebox--typography-style-signature-keys
|
||||
'(: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
|
||||
@ -196,8 +204,8 @@ Backend marker and buffer span data are intentionally excluded."
|
||||
(ebox-fragment-plist-keys new)))
|
||||
(cl-pushnew key keys))
|
||||
(dolist (key keys)
|
||||
(unless (equal (plist-get old key)
|
||||
(plist-get new key))
|
||||
(unless (ebox-style--declaration-value-equal-p
|
||||
(plist-get old key) (plist-get new key))
|
||||
(push key changed)))
|
||||
changed))
|
||||
|
||||
|
||||
49
ebox-grid.el
49
ebox-grid.el
@ -19,7 +19,8 @@
|
||||
(declare-function ebox--ensure-node-id "ebox" (node))
|
||||
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||
(declare-function ebox-get "ebox" (box property))
|
||||
(declare-function ebox-create "ebox" (&rest plist))
|
||||
(declare-function ebox-node-factory--create-from-properties
|
||||
"ebox-node-factory" (&rest plist))
|
||||
(declare-function ebox-render "ebox-layout" (node))
|
||||
(declare-function ebox--lines-concat-horizontal "ebox" (&rest strings))
|
||||
(declare-function ebox-string-lines "ebox" (string))
|
||||
@ -332,17 +333,10 @@ programmatic port and are therefore rejected here."
|
||||
(when (ebox-grid--visual-props-p props)
|
||||
(when (plist-member props :content)
|
||||
(error "ebox-grid: grid cannot combine :content with child nodes"))
|
||||
(apply #'ebox-create
|
||||
(apply #'ebox-node-factory--create-from-properties
|
||||
(append (ebox--preformatted-box-props props)
|
||||
(list :content "")))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-grid-fr (factor)
|
||||
"Return a fractional grid track with FACTOR weight."
|
||||
(unless (and (numberp factor) (> factor 0))
|
||||
(error "ebox-grid-fr: FACTOR must be positive: %S" factor))
|
||||
(list 'fr factor))
|
||||
|
||||
(defun ebox-grid--placement-part (value default)
|
||||
"Return (START SPAN) parsed from placement VALUE and DEFAULT."
|
||||
(cond
|
||||
@ -865,43 +859,6 @@ size for start/center/end alignment unless it would overflow its track."
|
||||
(ebox-grid--legacy-constraints props wrapper)
|
||||
(ebox-tree-layout-children node)))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-grid (&rest items)
|
||||
"Return a two-dimensional grid layout node from ITEMS.
|
||||
|
||||
Grid properties include `:grid-template-columns', `:grid-template-rows',
|
||||
`:grid-auto-flow', `:gap', `:grid-row-gap', `:grid-column-gap',
|
||||
`:justify-items', `:align-items', `:width', and `:height'. Track sizes use
|
||||
Ebox pixel/line units, `auto', `(fr FACTOR)', or symbols such as `1fr'.
|
||||
Children may carry `:grid-column', `:grid-row', `:grid-column-span', and
|
||||
`:grid-row-span' placement properties directly.
|
||||
`ebox-grid-fr' is a convenient constructor for fractional tracks."
|
||||
(let* ((split (ebox-grid--split-attrs items))
|
||||
(raw-props (car split))
|
||||
(children (delq nil (cdr split)))
|
||||
(box-props (cl-loop for (key value) on raw-props by #'cddr
|
||||
unless (memq key ebox--grid-layout-prop-keys)
|
||||
collect key and collect value))
|
||||
(wrapper (ebox-grid--wrapper box-props))
|
||||
(config
|
||||
(ebox-grid--normalize-config-props
|
||||
(ebox--plist-keep-keys raw-props ebox--grid-layout-prop-keys)))
|
||||
(node (list :ebox-type 'grid
|
||||
:display '(block grid)
|
||||
:props raw-props
|
||||
:raw-props raw-props
|
||||
:ebox-grid-config config
|
||||
:ebox-style-declarations
|
||||
(ebox-style-compile-declarations raw-props)
|
||||
:box wrapper
|
||||
:children children)))
|
||||
(when wrapper
|
||||
(plist-put wrapper :ebox-style-wrapper t))
|
||||
(dolist (key '(:id :class :selector-state :selector-attributes :key))
|
||||
(when (plist-member raw-props key)
|
||||
(plist-put node key (plist-get raw-props key))))
|
||||
node))
|
||||
|
||||
(provide 'ebox-grid)
|
||||
|
||||
;;; ebox-grid.el ends here
|
||||
|
||||
1621
ebox-incremental.el
1621
ebox-incremental.el
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,8 @@
|
||||
"ebox-flex" (props))
|
||||
(declare-function ebox-grid-layout-config-props-p
|
||||
"ebox-grid" (props))
|
||||
(declare-function ebox-flex-layout-create "ebox-flex" (&rest plist))
|
||||
(declare-function ebox-grid-layout-create "ebox-grid" (&rest plist))
|
||||
|
||||
(cl-defstruct
|
||||
(ebox-layout-config
|
||||
@ -102,6 +104,34 @@
|
||||
"Return canonical ColumnConfig from evaluated PLIST."
|
||||
(ebox-layout-config--simple-axis-create 'column plist))
|
||||
|
||||
(defun ebox-layout-config-property-names (tag)
|
||||
"Return canonical property names owned by TAG's LayoutConfig."
|
||||
(pcase tag
|
||||
('flex
|
||||
'(:flex-direction :flex-wrap :justify-content
|
||||
:align-items :align-content :row-gap :column-gap))
|
||||
('grid
|
||||
'(:grid-template-columns :grid-template-rows
|
||||
:grid-auto-columns :grid-auto-rows :grid-auto-flow
|
||||
:row-gap :column-gap
|
||||
:justify-items :align-items :justify-content :align-content))
|
||||
((or 'box 'row 'column) nil)
|
||||
(_ (error "Unknown Ebox Box author form: %S" tag))))
|
||||
|
||||
(defun ebox-layout-config-for-form (tag properties)
|
||||
"Return TAG's typed LayoutConfig from evaluated layout PROPERTIES."
|
||||
(pcase tag
|
||||
('box
|
||||
(when properties
|
||||
(error "Ebox Normal layout does not accept config properties: %S"
|
||||
properties))
|
||||
(ebox-normal-layout-create))
|
||||
('row (apply #'ebox-row-layout-create properties))
|
||||
('column (apply #'ebox-column-layout-create properties))
|
||||
('flex (apply #'ebox-flex-layout-create properties))
|
||||
('grid (apply #'ebox-grid-layout-create properties))
|
||||
(_ (error "Unknown Ebox Box author form: %S" tag))))
|
||||
|
||||
(provide 'ebox-layout-config)
|
||||
|
||||
;;; ebox-layout-config.el ends here
|
||||
|
||||
350
ebox-layout.el
350
ebox-layout.el
@ -190,10 +190,17 @@ value on an inner rendered surface wins over the enclosing box's value."
|
||||
"Render BOX by owning proven exact preformatted child content directly.
|
||||
Return nil unless BOX has no visual chrome or sizing behavior that requires
|
||||
the generic box pipeline."
|
||||
(when (and (not ebox--transparent-preformatted-box-fast-path-disabled)
|
||||
(ebox--box-content-node box)
|
||||
(let ((layout-kind (ebox--box-layout-kind box)))
|
||||
(when (and (not ebox--transparent-preformatted-box-fast-path-disabled)
|
||||
(or (ebox--box-content-node box)
|
||||
(memq layout-kind '(row column flex grid)))
|
||||
(not (eq (ebox-get box :visibility) 'hidden))
|
||||
(null (ebox-get box :wrap-mode))
|
||||
(or (ebox-style-no-soft-wrap-p (ebox-get box :wrap-mode))
|
||||
;; Typed Layout children are already a preformatted spatial
|
||||
;; result. Their owning Box must not send that result back
|
||||
;; through ordinary text wrapping merely because the public
|
||||
;; Box default is word wrapping.
|
||||
(memq layout-kind '(row column flex grid)))
|
||||
(eq (ebox-get box :text-align) 'left)
|
||||
(eq (ebox-get box :vertical-align) 'top)
|
||||
(null (ebox-get box :height))
|
||||
@ -212,7 +219,8 @@ the generic box pipeline."
|
||||
(equal (ebox-get box :border-right-pixel) 0)
|
||||
(not (ebox-get box :border-top-p))
|
||||
(not (ebox-get box :border-bottom-p))
|
||||
(null (ebox-get box :color))
|
||||
(null (and (ebox-style--text-paint-owner-p box)
|
||||
(ebox-get box :color)))
|
||||
(null (ebox-get box :bgcolor)))
|
||||
(let* ((content (ebox--box-content box))
|
||||
(known-width
|
||||
@ -232,7 +240,7 @@ the generic box pipeline."
|
||||
(puthash region-id box ebox--region-box-table)
|
||||
(unless ebox--intrinsic-layout-measurement
|
||||
(ebox--scroll-clear-state region-id))
|
||||
(ebox--record-rendered-uniform-width rendered known-width))))))
|
||||
(ebox--record-rendered-uniform-width rendered known-width)))))))
|
||||
|
||||
(defun ebox--render-flat-preformatted-box (box)
|
||||
"Render simple BOX chrome in a single line-oriented pass.
|
||||
@ -253,7 +261,8 @@ and its formatted content exactly fills the used height."
|
||||
(equal (ebox-get box :margin-bottom-height) 0))
|
||||
(let* ((content (ebox--box-content box))
|
||||
(exact-p (and (plist-get box :ebox-content-width-exact-p)
|
||||
(null (ebox-get box :wrap-mode))
|
||||
(ebox-style-no-soft-wrap-p
|
||||
(ebox-get box :wrap-mode))
|
||||
(eq (ebox-get box :text-align) 'left)))
|
||||
(formatted (if exact-p
|
||||
content
|
||||
@ -287,7 +296,9 @@ and its formatted content exactly fills the used height."
|
||||
(ebox-lines-join
|
||||
(ebox--render-underfilled-simple-scroll-lines
|
||||
lines region-id content-height padding-line-filler
|
||||
(ebox-get box :color) (ebox-get box :bgcolor)))
|
||||
(and (ebox-style--text-paint-owner-p box)
|
||||
(ebox-get box :color))
|
||||
(ebox-get box :bgcolor)))
|
||||
(ebox-lines-join
|
||||
(ebox--window-render-flat-wrapper-chunk-lines
|
||||
box lines 0 region-id padding-line-filler t t)))))
|
||||
@ -394,7 +405,15 @@ Single-child wrappers are transparent to this scheduling boundary."
|
||||
|
||||
(defun ebox--render-node-window-lines (node limit)
|
||||
"Return NODE's lazy window lines through the registered type renderer."
|
||||
(when-let* ((type (and (listp node) (plist-get node :ebox-type)))
|
||||
(when-let* ((type
|
||||
(and (listp node)
|
||||
(if (and (eq (plist-get node :ebox-kind) 'box)
|
||||
(when-let* ((config
|
||||
(plist-get node
|
||||
:ebox-layout-config)))
|
||||
(eq (ebox-layout-config-kind config) 'flex)))
|
||||
'flex
|
||||
(plist-get node :ebox-type))))
|
||||
(renderer (gethash type ebox--window-line-renderer-table)))
|
||||
(funcall renderer node limit)))
|
||||
|
||||
@ -428,7 +447,8 @@ Single-child wrappers are transparent to this scheduling boundary."
|
||||
START-INDEX is the first source line index represented by LINES."
|
||||
(let* ((padding-line-filler
|
||||
(ebox-pixel-space (ebox--content-pixel box)))
|
||||
(color (ebox-get box :color))
|
||||
(color (and (ebox-style--text-paint-owner-p box)
|
||||
(ebox-get box :color)))
|
||||
(bgcolor (ebox-get box :bgcolor))
|
||||
(rendered
|
||||
(cl-loop for line in lines
|
||||
@ -729,7 +749,7 @@ Returns a list of strings (lines)."
|
||||
When WRAP-MODE is nil, return TEXT without pre-wrapping.
|
||||
When WRAP-MODE is `kp', use Knuth-Plass algorithm via ekp package."
|
||||
(cond
|
||||
((null wrap-mode)
|
||||
((memq wrap-mode '(nil none))
|
||||
text)
|
||||
((eq wrap-mode 'kp)
|
||||
(if (and (require 'ekp nil t)
|
||||
@ -942,39 +962,71 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
||||
(not (eq (and (listp node) (plist-get node :ebox-type)) 'box)))
|
||||
content))
|
||||
|
||||
(defun ebox--normal-text-units (string)
|
||||
"Return word/space/break units for inline Text STRING."
|
||||
(defun ebox--normal-text-units (string wrap-mode)
|
||||
"Return inline flow units for Text STRING under WRAP-MODE."
|
||||
(let ((lines (ebox-string-lines string)) units)
|
||||
(cl-loop for line in lines
|
||||
for line-tail on lines
|
||||
do (let ((position 0)
|
||||
(limit (length line)))
|
||||
(while (< position limit)
|
||||
(let* ((space-p
|
||||
(not (null
|
||||
(memq (aref line position) '(?\s ?\t)))))
|
||||
(end position))
|
||||
(while (and (< end limit)
|
||||
(eq (not (null
|
||||
(memq (aref line end)
|
||||
'(?\s ?\t))))
|
||||
space-p))
|
||||
(setq end (1+ end)))
|
||||
(let ((part (substring line position end)))
|
||||
(push (list :kind (if space-p 'space 'word)
|
||||
:rendered part
|
||||
:width (ebox--string-pixel-width part))
|
||||
units))
|
||||
(setq position end))))
|
||||
do (if (eq wrap-mode 'char)
|
||||
(let ((clusters (ebox--grapheme-clusters line)))
|
||||
(dotimes (index (length clusters))
|
||||
(let* ((cluster (aref clusters index))
|
||||
(part (substring
|
||||
line
|
||||
(plist-get cluster :start)
|
||||
(plist-get cluster :end))))
|
||||
(push (list :kind 'char :rendered part
|
||||
:width (plist-get cluster :width))
|
||||
units))))
|
||||
(let ((position 0)
|
||||
(limit (length line)))
|
||||
(while (< position limit)
|
||||
(let* ((space-p
|
||||
(not (null
|
||||
(memq (aref line position) '(?\s ?\t)))))
|
||||
(end position))
|
||||
(while (and (< end limit)
|
||||
(eq (not (null
|
||||
(memq (aref line end)
|
||||
'(?\s ?\t))))
|
||||
space-p))
|
||||
(setq end (1+ end)))
|
||||
(let ((part (substring line position end)))
|
||||
(push (list :kind (if space-p 'space 'word)
|
||||
:rendered part
|
||||
:width (ebox--string-pixel-width part))
|
||||
units))
|
||||
(setq position end)))))
|
||||
unless (null (cdr line-tail))
|
||||
do (push '(:kind break) units))
|
||||
(nreverse units)))
|
||||
|
||||
(defun ebox--render-normal-box-children (box children)
|
||||
"Render canonical CHILDREN without flattening inline Box boundaries."
|
||||
(catch 'rendered
|
||||
(when (and (= (length children) 1)
|
||||
(eq (plist-get (car children) :ebox-kind) 'text)
|
||||
(not (eq (ebox-get box :wrap-mode) 'kp)))
|
||||
(throw
|
||||
'rendered
|
||||
(ebox--call-with-box-content-context
|
||||
box
|
||||
(lambda ()
|
||||
(let* ((child (car children))
|
||||
(width (ebox--wrapper-content-viewport-pixel box))
|
||||
(wrap-mode (ebox-get box :wrap-mode))
|
||||
(rendered
|
||||
(let ((ebox--inline-auto-width-intrinsic-p t))
|
||||
(ebox--render-with-cache child))))
|
||||
(if (and width (> width 0)
|
||||
(ebox-style-soft-wrap-p wrap-mode))
|
||||
(ebox--wrap-text rendered width wrap-mode)
|
||||
rendered))))))
|
||||
(let* ((resolved-width (ebox--wrapper-content-viewport-pixel box))
|
||||
(content-width (and resolved-width (> resolved-width 0)
|
||||
resolved-width))
|
||||
(wrap-mode (ebox-get box :wrap-mode))
|
||||
(soft-wrap-p (not (memq wrap-mode '(nil none kp))))
|
||||
(line-width 0)
|
||||
pending-empty-line
|
||||
line-parts segments)
|
||||
@ -988,7 +1040,7 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
||||
(setq line-parts nil line-width 0)))
|
||||
(append-atomic
|
||||
(rendered width)
|
||||
(when (and content-width line-parts
|
||||
(when (and soft-wrap-p content-width line-parts
|
||||
(> (+ line-width width) content-width))
|
||||
(flush-line))
|
||||
(push rendered line-parts)
|
||||
@ -996,7 +1048,8 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
||||
pending-empty-line nil))
|
||||
(append-word
|
||||
(rendered width)
|
||||
(if (or (null content-width) (<= width content-width))
|
||||
(if (or (not soft-wrap-p)
|
||||
(null content-width) (<= width content-width))
|
||||
(append-atomic rendered width)
|
||||
(let ((parts (ebox--wrap-line rendered content-width 'char)))
|
||||
(cl-loop for part in parts
|
||||
@ -1033,7 +1086,7 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
||||
('space
|
||||
(let ((width (plist-get unit :width)))
|
||||
(cond
|
||||
((and content-width (> width content-width))
|
||||
((and soft-wrap-p content-width (> width content-width))
|
||||
(let ((rendered (plist-get unit :rendered)))
|
||||
(if (and (= (length rendered) 1)
|
||||
(get-text-property 0 'display rendered))
|
||||
@ -1041,12 +1094,16 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
||||
(append-word rendered width))))
|
||||
((null line-parts)
|
||||
(append-atomic (plist-get unit :rendered) width))
|
||||
((and content-width (> (+ line-width width) content-width))
|
||||
((and soft-wrap-p content-width
|
||||
(> (+ line-width width) content-width))
|
||||
(flush-line))
|
||||
(t (append-atomic (plist-get unit :rendered) width)))))
|
||||
('word
|
||||
(append-word (plist-get unit :rendered)
|
||||
(plist-get unit :width)))
|
||||
('char
|
||||
(append-word (plist-get unit :rendered)
|
||||
(plist-get unit :width)))
|
||||
('box
|
||||
(append-atomic (plist-get unit :rendered)
|
||||
(plist-get unit :width)))))
|
||||
@ -1063,7 +1120,7 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
||||
(setq pending-empty-line nil)
|
||||
(push rendered segments))
|
||||
(if (eq (plist-get child :ebox-kind) 'text)
|
||||
(dolist (unit (ebox--normal-text-units rendered))
|
||||
(dolist (unit (ebox--normal-text-units rendered wrap-mode))
|
||||
(append-unit unit))
|
||||
(append-unit
|
||||
(list :kind 'box :rendered rendered
|
||||
@ -1076,12 +1133,12 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
||||
(flush-line))
|
||||
(if segments
|
||||
(apply #'ebox--lines-stack-vertical (nreverse segments))
|
||||
"")))
|
||||
""))))
|
||||
|
||||
(defun ebox--render-box-layout-children (box)
|
||||
"Render canonical BOX children through its typed LayoutConfig."
|
||||
(let* ((kind (ebox--box-layout-kind box))
|
||||
(children (plist-get box :children))
|
||||
(children (ebox-tree-layout-children box))
|
||||
(content
|
||||
(ebox--render-box-content-cached
|
||||
box
|
||||
@ -1116,6 +1173,9 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
||||
(plist-get box :ebox-layout-config))
|
||||
children))
|
||||
(_ (error "Ebox Box has unsupported LayoutConfig: %S" kind)))))))
|
||||
(plist-put box :ebox-content-layout-complete-p
|
||||
(not (and (eq kind 'normal)
|
||||
(eq (ebox-get box :wrap-mode) 'kp))))
|
||||
(ebox--record-box-content-width-exact
|
||||
box content (or (memq kind '(row column flex grid))
|
||||
(consp (cdr children))))
|
||||
@ -1123,14 +1183,20 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
||||
|
||||
(defun ebox--box-content (box)
|
||||
"Return BOX content, rendering any lazy child layout content if present."
|
||||
(ebox--propertize-typography
|
||||
(cond
|
||||
((plist-member box :children)
|
||||
(ebox--render-box-layout-children box))
|
||||
((when-let* ((node (ebox--box-content-node box)))
|
||||
(ebox--render-box-content-node box node)))
|
||||
(t (ebox-get box :content)))
|
||||
box))
|
||||
(let ((content
|
||||
(cond
|
||||
((or (plist-member box :children)
|
||||
(plist-get box :ebox-child-sequence))
|
||||
(ebox--render-box-layout-children box))
|
||||
((when-let* ((node (ebox--box-content-node box)))
|
||||
(ebox--render-box-content-node box node)))
|
||||
(t (ebox-get box :content)))))
|
||||
;; Canonical Text leaves materialize the inherited font exactly once.
|
||||
;; A canonical Box owns geometry/background and must not wrap the same
|
||||
;; typography contribution around already-rendered descendants.
|
||||
(if (eq (plist-get box :ebox-kind) 'box)
|
||||
content
|
||||
(ebox--propertize-typography content box))))
|
||||
|
||||
(defun ebox--line-min-content-pixel (line)
|
||||
"Return LINE's min-content width in content pixels."
|
||||
@ -1172,7 +1238,7 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
||||
(defun ebox--content-min-pixel (box)
|
||||
"Return BOX's min-content width in content pixels."
|
||||
(let ((wrap-mode (ebox-get box :wrap-mode)))
|
||||
(if (null wrap-mode)
|
||||
(if (ebox-style-no-soft-wrap-p wrap-mode)
|
||||
(ebox--content-max-pixel box)
|
||||
(let ((lines (ebox-string-lines (ebox--box-content box)))
|
||||
(max-width 0))
|
||||
@ -1361,7 +1427,7 @@ FALLBACK is used for nil and auto values."
|
||||
(known-width
|
||||
(gethash content ebox--rendered-uniform-width-table)))
|
||||
(if (and (eq align 'left)
|
||||
(or (and (null wrap-mode)
|
||||
(or (and (ebox-style-no-soft-wrap-p wrap-mode)
|
||||
(or (plist-get box :ebox-content-width-exact-p)
|
||||
(and (numberp known-width)
|
||||
(= known-width
|
||||
@ -1371,7 +1437,10 @@ FALLBACK is used for nil and auto values."
|
||||
(= known-width (ebox--content-pixel box)))))
|
||||
content
|
||||
(let* ((pixel-width (ebox--content-pixel box))
|
||||
(wrapped-content (ebox--wrap-text content pixel-width wrap-mode)))
|
||||
(wrapped-content
|
||||
(if (plist-get box :ebox-content-layout-complete-p)
|
||||
content
|
||||
(ebox--wrap-text content pixel-width wrap-mode))))
|
||||
;; kp mode distributes inner gaps, but Ebox still owns the box geometry.
|
||||
;; Pad ragged lines (notably the last line) back to the content width
|
||||
;; while preserving the requested alignment for those lines.
|
||||
@ -1449,7 +1518,8 @@ Internal implementation of `ebox-render' for box nodes."
|
||||
(border-bottom-color (ebox-get box :border-bottom-color))
|
||||
|
||||
;; Style and Layout
|
||||
(color (ebox-get box :color))
|
||||
(color (and (ebox-style--text-paint-owner-p box)
|
||||
(ebox-get box :color)))
|
||||
(bgcolor (ebox-get box :bgcolor))
|
||||
(inner-height (+ content-height padding-top padding-bottom))
|
||||
(padding-line-filler (ebox-pixel-space content-pixel))
|
||||
@ -1767,8 +1837,31 @@ Internal implementation of `ebox-render' for box nodes."
|
||||
(defvar ebox--surface-materialization-active nil
|
||||
"Non-nil while an Ebox TP producer is running raw layout.")
|
||||
|
||||
(defvar ebox--render-root-parent-kind nil
|
||||
"Formatting context of a detached root rendered for its owning parent.")
|
||||
(defun ebox--render-text-node (node)
|
||||
"Render canonical TextNode NODE without entering Box geometry."
|
||||
(let* ((region-id (or ebox--render-region-id
|
||||
(ebox--ensure-region-id node)))
|
||||
(value (copy-sequence (plist-get node :ebox-text-value)))
|
||||
(styled (ebox--propertize-typography value node))
|
||||
(painted
|
||||
(if (or (ebox-get node :color) (ebox-get node :bgcolor))
|
||||
(ebox--propertize-colors
|
||||
styled (ebox-get node :color) (ebox-get node :bgcolor))
|
||||
styled))
|
||||
(surfaced
|
||||
(ebox--apply-surface-properties
|
||||
painted (ebox-get node :surface-properties)))
|
||||
(fallback (ebox-pixel-space 0))
|
||||
(rendered
|
||||
(ebox-lines-join
|
||||
(cl-loop for line in (ebox-string-lines surfaced)
|
||||
for index from 0
|
||||
collect (ebox--propertize-content-line
|
||||
line region-id index fallback)))))
|
||||
(puthash region-id node ebox--region-box-table)
|
||||
(unless ebox--intrinsic-layout-measurement
|
||||
(ebox--scroll-clear-state region-id))
|
||||
rendered))
|
||||
|
||||
(defun ebox--render-layout (node)
|
||||
"Render layout NODE directly to a multi-line propertized string.
|
||||
@ -1786,12 +1879,13 @@ This function owns spatial layout only; it never creates a live TP surface."
|
||||
(ebox--ensure-node-id node))
|
||||
(let ((type (plist-get node :ebox-type)))
|
||||
(cond
|
||||
((eq (plist-get node :ebox-kind) 'text)
|
||||
(ebox--render-text-node node))
|
||||
((eq type 'box) (ebox--render-box node))
|
||||
((eq type 'concat) (ebox--render-concat node))
|
||||
((eq type 'stack) (ebox--render-stack node))
|
||||
((eq type 'flex) (ebox--render-flex node))
|
||||
((eq type 'grid) (ebox--render-grid node))
|
||||
((eq type 'flex-item) (ebox--render-layout (plist-get node :node)))
|
||||
(t (error "ebox-render: unknown node type %S" type)))))))
|
||||
|
||||
(declare-function ebox-surface-producer
|
||||
@ -1840,6 +1934,7 @@ materialized surface is ephemeral and creates no live buffer state."
|
||||
(require 'ebox-surface)
|
||||
(if (and (not (stringp node))
|
||||
(not (ebox-style-cascade-active-p))
|
||||
(zerop (ebox-tree-author-style-count node))
|
||||
(not (ebox-surface--inline-inheritance-required-p node)))
|
||||
(let ((ebox--region-id-counter ebox--region-id-counter)
|
||||
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
|
||||
@ -1876,42 +1971,6 @@ The blank lines match the pixel width of the first line of STRING."
|
||||
(blank (ebox-pixel-space px)))
|
||||
(ebox-lines-join (append lines (make-list extra blank)))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-concat (node1 node2)
|
||||
"Return a lazy concat node that places NODE1 and NODE2 side by side.
|
||||
NODE1 and NODE2 must be box/concat/stack nodes created by
|
||||
`ebox-create', `ebox-concat', or `ebox-stack'.
|
||||
The result is not rendered until `ebox-render' or `ebox-render-to-buffer'
|
||||
is called. To compose more than two boxes, nest calls:
|
||||
(ebox-concat a (ebox-concat b c))"
|
||||
(list :ebox-type 'concat
|
||||
:display '(block row)
|
||||
:children (delq nil (list node1 node2))
|
||||
:left node1
|
||||
:right node2))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-spacer (&rest plist)
|
||||
"Return a blank box node.
|
||||
PLIST accepts the same properties as `ebox-create', such as :width,
|
||||
:height, :margin, or :bgcolor."
|
||||
(apply #'ebox-create :content "" plist))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-row (&rest nodes)
|
||||
"Return a lazy horizontal layout node from NODES.
|
||||
Nil entries are ignored. With zero non-nil nodes, return `ebox-spacer'.
|
||||
A sole child Range keeps a material row parent so the Range stays addressable."
|
||||
(setq nodes (delq nil nodes))
|
||||
(cond
|
||||
((null nodes) (ebox-spacer))
|
||||
((and (null (cdr nodes))
|
||||
(not (ebox-child-range--descriptor-p (car nodes))))
|
||||
(car nodes))
|
||||
(t (list :ebox-type 'concat
|
||||
:display '(block row)
|
||||
:children nodes))))
|
||||
|
||||
(defun ebox--render-row-children (children &optional config)
|
||||
"Render flat row CHILDREN with optional typed Row CONFIG.
|
||||
Auto-width children are rendered at intrinsic inline size while explicit
|
||||
@ -1953,45 +2012,24 @@ horizontal concatenation."
|
||||
"Render a legacy concat NODE to a string."
|
||||
(ebox--render-row-children (ebox--layout-children node)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-stack (node1 node2)
|
||||
"Return a lazy stack node that places NODE1 above NODE2 vertically.
|
||||
NODE1 and NODE2 must be box/concat/stack nodes created by
|
||||
`ebox-create', `ebox-concat', or `ebox-stack'.
|
||||
The result is not rendered until `ebox-render' or `ebox-render-to-buffer'
|
||||
is called. To stack more than two boxes, nest calls:
|
||||
(ebox-stack a (ebox-stack b c))"
|
||||
(list :ebox-type 'stack
|
||||
:display '(block column)
|
||||
:children (delq nil (list node1 node2))
|
||||
:top node1
|
||||
:bottom node2))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-column (&rest nodes)
|
||||
"Return a lazy vertical layout node from NODES.
|
||||
Nil entries are ignored. With zero non-nil nodes, return `ebox-spacer'.
|
||||
A sole child Range keeps a material column parent so the Range stays
|
||||
addressable."
|
||||
(setq nodes (delq nil nodes))
|
||||
(cond
|
||||
((null nodes) (ebox-spacer))
|
||||
((and (null (cdr nodes))
|
||||
(not (ebox-child-range--descriptor-p (car nodes))))
|
||||
(car nodes))
|
||||
(t (list :ebox-type 'stack
|
||||
:display '(block column)
|
||||
:children nodes))))
|
||||
|
||||
(defun ebox--stack-leaves (node)
|
||||
"Return NODE's vertical stack leaves in render order."
|
||||
(let (leaves)
|
||||
(cl-labels ((collect (current)
|
||||
(if (and (listp current)
|
||||
(eq (plist-get current :ebox-type) 'stack))
|
||||
(dolist (child (ebox--layout-children current))
|
||||
(collect child))
|
||||
(push current leaves))))
|
||||
(cond
|
||||
((and (listp current)
|
||||
(eq (plist-get current :ebox-type) 'stack))
|
||||
(dolist (child (ebox--layout-children current))
|
||||
(collect child)))
|
||||
((ebox--scroll-window-canonical-column-p current)
|
||||
;; A canonical Column is itself the formatting context.
|
||||
;; Its direct children are the lazy sequence; nested
|
||||
;; Columns remain material leaves because they may own
|
||||
;; independent Box chrome.
|
||||
(dolist (child (ebox-tree-layout-children current))
|
||||
(push child leaves)))
|
||||
(t
|
||||
(push current leaves)))))
|
||||
(collect node))
|
||||
(nreverse leaves)))
|
||||
|
||||
@ -2664,11 +2702,55 @@ last cached leaf instead of walking the stack from the beginning."
|
||||
:complete complete-cache
|
||||
:box box))))))))
|
||||
|
||||
(defun ebox--scroll-window-canonical-column-p (node)
|
||||
"Return non-nil for the Column subset supported by lazy scroll layout."
|
||||
(and (eq (plist-get node :ebox-kind) 'box)
|
||||
(when-let* ((config (plist-get node :ebox-layout-config)))
|
||||
(and (eq (ebox-layout-config-kind config) 'column)
|
||||
(let ((props (ebox-layout-config-props config)))
|
||||
(and (zerop (or (plist-get props :item-gap) 0))
|
||||
(eq (or (plist-get props :cross-align) 'stretch)
|
||||
'stretch)))))))
|
||||
|
||||
(defun ebox--scroll-window-transparent-column-p (node)
|
||||
"Return non-nil when NODE's Column Box adds no visible wrapper chrome."
|
||||
(and (ebox--scroll-window-canonical-column-p node)
|
||||
(null (ebox-get node :height))
|
||||
(zerop (or (ebox-get node :min-height) 0))
|
||||
(null (ebox-get node :max-height))
|
||||
(cl-every
|
||||
(lambda (property) (zerop (or (ebox-get node property) 0)))
|
||||
'(: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))
|
||||
(not (ebox-get node :border-top-p))
|
||||
(not (ebox-get node :border-bottom-p))
|
||||
(null (ebox-get node :color))
|
||||
(null (ebox-get node :bgcolor))
|
||||
(null (ebox-get node :font))
|
||||
(null (ebox-get node :font-family))
|
||||
(null (ebox-get node :font-height))
|
||||
(null (ebox-get node :font-weight))
|
||||
(null (ebox-get node :font-slant))
|
||||
(null (ebox-get node :surface-properties))
|
||||
(eq (ebox-get node :visibility) 'visible)
|
||||
(eq (ebox-get node :text-align) 'left)
|
||||
(eq (ebox-get node :vertical-align) 'top)))
|
||||
|
||||
(defun ebox--scroll-window-stack-source (node)
|
||||
"Return lazy scroll stack source metadata for NODE, or nil."
|
||||
(cond
|
||||
((eq (plist-get node :ebox-type) 'stack)
|
||||
(list :stack node))
|
||||
((ebox--scroll-window-canonical-column-p node)
|
||||
(list :stack node))
|
||||
((and (eq (plist-get node :ebox-kind) 'box)
|
||||
(when-let* ((children (ebox-tree-layout-children node)))
|
||||
(and (= (length children) 1)
|
||||
(ebox--scroll-window-transparent-column-p (car children)))))
|
||||
(list :stack (car (ebox-tree-layout-children node))))
|
||||
((and (eq (plist-get node :ebox-type) 'box)
|
||||
(eq (plist-get (plist-get node :ebox-content-node)
|
||||
:ebox-type)
|
||||
@ -2689,8 +2771,9 @@ are always resolved from BOX so no closure can retain a replaced runtime."
|
||||
(plist-get spec :materialize-viewport-width))
|
||||
(viewport-height (plist-get spec :viewport-height))
|
||||
(source
|
||||
(ebox--scroll-window-stack-source
|
||||
(ebox--box-content-node box))))
|
||||
(or (ebox--scroll-window-stack-source
|
||||
(ebox--box-content-node box))
|
||||
(ebox--scroll-window-stack-source box))))
|
||||
(unless (and (numberp prefix-viewport-width)
|
||||
(> prefix-viewport-width 0)
|
||||
(numberp materialize-viewport-width)
|
||||
@ -2722,7 +2805,7 @@ are always resolved from BOX so no closure can retain a replaced runtime."
|
||||
(defun ebox--scroll-window-source-lines-exact-p (box window)
|
||||
"Return non-nil when WINDOW lines can be used as BOX formatted content."
|
||||
(and (plist-get window :content-width-exact-p)
|
||||
(null (ebox-get box :wrap-mode))
|
||||
(ebox-style-no-soft-wrap-p (ebox-get box :wrap-mode))
|
||||
(eq (ebox-get box :text-align) 'left)
|
||||
(= (ebox--content-pixel box)
|
||||
(or (ebox--viewport-pixel-width nil) 0))))
|
||||
@ -2744,7 +2827,7 @@ are always resolved from BOX so no closure can retain a replaced runtime."
|
||||
(or (null (ebox-get box :min-height))
|
||||
(equal (ebox-get box :min-height) 0))
|
||||
(null (ebox-get box :max-height))
|
||||
(null (ebox-get box :wrap-mode))
|
||||
(ebox-style-no-soft-wrap-p (ebox-get box :wrap-mode))
|
||||
(eq (ebox-get box :text-align) 'left)
|
||||
(eq (ebox-get box :vertical-align) 'top))))
|
||||
|
||||
@ -2783,7 +2866,8 @@ no included vertical margin."
|
||||
(border-bottom-p
|
||||
(and include-bottom (ebox-get box :border-bottom-p)))
|
||||
(border-bottom-color (ebox-get box :border-bottom-color))
|
||||
(color (ebox-get box :color))
|
||||
(color (and (ebox-style--text-paint-owner-p box)
|
||||
(ebox-get box :color)))
|
||||
(bgcolor (ebox-get box :bgcolor))
|
||||
(padding-left-line
|
||||
(when (> padding-left 0)
|
||||
@ -2966,7 +3050,8 @@ only used for incomplete lazy prefixes."
|
||||
(border-top-p (and include-top
|
||||
(ebox-get box :border-top-p)))
|
||||
(border-top-color (ebox-get box :border-top-color))
|
||||
(color (ebox-get box :color))
|
||||
(color (and (ebox-style--text-paint-owner-p box)
|
||||
(ebox-get box :color)))
|
||||
(bgcolor (ebox-get box :bgcolor))
|
||||
(inner-height (+ (length content-lines) padding-top))
|
||||
result)
|
||||
@ -3250,8 +3335,9 @@ only used for incomplete lazy prefixes."
|
||||
(let ((content-height (ebox--scroll-window-content-height box))
|
||||
(scroll-offset (max 0 (or (ebox-get box :scroll-offset) 0)))
|
||||
(content-viewport (ebox--wrapper-content-viewport-pixel box))
|
||||
(source (ebox--scroll-window-stack-source
|
||||
(ebox--box-content-node box))))
|
||||
(source (or (ebox--scroll-window-stack-source
|
||||
(ebox--box-content-node box))
|
||||
(ebox--scroll-window-stack-source box))))
|
||||
(when (and (not ebox--scroll-window-render-disabled)
|
||||
(eq (ebox-get box :overflow) 'scroll)
|
||||
(ebox--viewport-height-size-value-p (ebox-get box :height))
|
||||
|
||||
@ -232,6 +232,19 @@ keep their absolute pixel width."
|
||||
(puthash string width cache))
|
||||
width)))))
|
||||
|
||||
(defun ebox--measurement-face-remapping ()
|
||||
"Return face remaps that can affect geometric text measurement.
|
||||
TP paint slots are stable paint-layer addresses. Their foreground/background
|
||||
updates change redisplay without changing glyph geometry, so installing or
|
||||
switching those layers must not invalidate Ebox measurement caches."
|
||||
(when (boundp 'face-remapping-alist)
|
||||
(cl-remove-if
|
||||
(lambda (entry)
|
||||
(let ((face (car-safe entry)))
|
||||
(and (symbolp face)
|
||||
(string-match-p "\\`tp-paint-slot-" (symbol-name face)))))
|
||||
face-remapping-alist)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-string-pixel-width (string)
|
||||
"Return the display pixel width of STRING's first line.
|
||||
@ -251,7 +264,7 @@ This is the public, text-scale-aware measurement used by Ebox layout code."
|
||||
(face-attribute 'default :width nil t)
|
||||
(face-attribute 'default :foreground nil t)
|
||||
(face-attribute 'default :background nil t)
|
||||
(and (boundp 'face-remapping-alist) face-remapping-alist)
|
||||
(ebox--measurement-face-remapping)
|
||||
(and (boundp 'text-scale-mode-amount) text-scale-mode-amount)))
|
||||
|
||||
(defsubst ebox--current-display-signature ()
|
||||
|
||||
@ -20,6 +20,8 @@
|
||||
(session node frame &optional layout-package state))
|
||||
(declare-function ebox-native-reflow-create-session
|
||||
"ebox-native-reflow" (&rest arguments))
|
||||
(declare-function ebox-native-reflow-fork-session
|
||||
"ebox-native-reflow" (session &rest options))
|
||||
(declare-function ebox-native-reflow-confirm-native-frame
|
||||
"ebox-native-reflow"
|
||||
(session generation key confirmed-revision))
|
||||
@ -232,6 +234,59 @@ type, key, parent, and sibling position are identical."
|
||||
(ebox-native-commit--runtime-types-supported-p state)
|
||||
(ebox-native-commit--region-index-safe-p state))))
|
||||
|
||||
(defun ebox-native-commit-viewport-continuity-eligible-p
|
||||
(previous-state candidate-state)
|
||||
"Return non-nil when CANDIDATE-STATE can extend the retained native frame.
|
||||
The viewport planner owns the unchanged-topology proof. This predicate owns
|
||||
the native continuation contract: a live session, a confirmable prior frame,
|
||||
the committed text/fragment bases, compiler postorder, and projection support."
|
||||
(and (eq (plist-get previous-state :projection-kind) 'native-frame)
|
||||
(plist-get previous-state :native-sync-session)
|
||||
(or (plist-get previous-state :native-sync-pending)
|
||||
(plist-get previous-state :native-sync-confirmed-p))
|
||||
(stringp (plist-get previous-state :native-committed-rendered))
|
||||
(vectorp
|
||||
(plist-get previous-state :native-committed-fragment-template))
|
||||
(plist-member previous-state :native-committed-owned-ranges)
|
||||
(let ((postorder (plist-get previous-state :native-node-postorder)))
|
||||
(and (vectorp postorder) (> (length postorder) 0)))
|
||||
(let ((state (copy-sequence candidate-state)))
|
||||
(plist-put state :native-topology-stable-p t)
|
||||
;; Viewport context changed, but no declarative source node did.
|
||||
;; Presence of this empty set is an explicit compiler certificate.
|
||||
(plist-put state :native-touched-node-ids nil)
|
||||
(plist-put state :native-removed-node-ids nil)
|
||||
(ebox-native-commit-projection-eligible-p
|
||||
previous-state state nil))))
|
||||
|
||||
(defun ebox-native-commit--isolate-session
|
||||
(previous-state candidate-state)
|
||||
"Give CANDIDATE-STATE a private child of PREVIOUS-STATE's native session."
|
||||
(when-let* ((session (plist-get previous-state :native-sync-session))
|
||||
(fork
|
||||
(condition-case nil
|
||||
(ebox-native-reflow-fork-session session)
|
||||
(error nil))))
|
||||
(plist-put candidate-state :native-sync-session fork)
|
||||
;; A pending/confirmed frame is local to the previous Rust handle. The
|
||||
;; fork retains compiler facts but begins its own publication sequence.
|
||||
(plist-put candidate-state :native-sync-pending nil)
|
||||
(plist-put candidate-state :native-sync-confirmed-p nil)
|
||||
t))
|
||||
|
||||
(defun ebox-native-commit-prepare-viewport-continuity
|
||||
(previous-state candidate-state)
|
||||
"Prepare a private native viewport continuation in CANDIDATE-STATE."
|
||||
(and (ebox-native-commit-viewport-continuity-eligible-p
|
||||
previous-state candidate-state)
|
||||
(ebox-native-commit--isolate-session
|
||||
previous-state candidate-state)
|
||||
(progn
|
||||
(plist-put candidate-state :native-topology-stable-p t)
|
||||
(plist-put candidate-state :native-touched-node-ids nil)
|
||||
(plist-put candidate-state :native-removed-node-ids nil)
|
||||
t)))
|
||||
|
||||
(defun ebox-native-commit-plan-eligible-p
|
||||
(old-state candidate-state prepared)
|
||||
"Return non-nil when PREPARED can bypass superseded local layout proofs."
|
||||
@ -249,22 +304,25 @@ type, key, parent, and sibling position are identical."
|
||||
(ebox-native-commit-object-delta-node-ids
|
||||
old-state candidate-state prepared)))
|
||||
(state (copy-sequence candidate-state)))
|
||||
(plist-put candidate-state :native-topology-stable-p stable)
|
||||
(plist-put candidate-state :native-touched-node-ids layout-touched)
|
||||
(when context-axes-stable
|
||||
(plist-put candidate-state :viewport-dependent-node-ids-ready t)
|
||||
(plist-put candidate-state :viewport-dependent-node-ids
|
||||
(plist-get old-state :viewport-dependent-node-ids))
|
||||
(plist-put candidate-state :viewport-dependent-node-id-axes
|
||||
(plist-get old-state :viewport-dependent-node-id-axes)))
|
||||
(unless stable
|
||||
(cl-remf candidate-state :native-fragment-owner-cache))
|
||||
(plist-put state :native-touched-node-ids layout-touched)
|
||||
(plist-put state :native-topology-stable-p stable)
|
||||
(and (or stable layout-touched)
|
||||
(not (ebox-style-cascade-active-p))
|
||||
(ebox-native-commit-projection-eligible-p
|
||||
old-state state nil))))
|
||||
(when (and (or stable layout-touched)
|
||||
(not (ebox-style-cascade-active-p))
|
||||
(ebox-native-commit-projection-eligible-p
|
||||
old-state state nil)
|
||||
(ebox-native-commit--isolate-session
|
||||
old-state candidate-state))
|
||||
(plist-put candidate-state :native-topology-stable-p stable)
|
||||
(plist-put candidate-state :native-touched-node-ids layout-touched)
|
||||
(when context-axes-stable
|
||||
(plist-put candidate-state :viewport-dependent-node-ids-ready t)
|
||||
(plist-put candidate-state :viewport-dependent-node-ids
|
||||
(plist-get old-state :viewport-dependent-node-ids))
|
||||
(plist-put candidate-state :viewport-dependent-node-id-axes
|
||||
(plist-get old-state :viewport-dependent-node-id-axes)))
|
||||
(unless stable
|
||||
(cl-remf candidate-state :native-fragment-owner-cache))
|
||||
t)))
|
||||
|
||||
(defun ebox-native-commit--root-box (node)
|
||||
"Return NODE's outer box, or nil."
|
||||
@ -309,7 +367,7 @@ type, key, parent, and sibling position are identical."
|
||||
(maphash
|
||||
(lambda (_node-id node)
|
||||
(unless (memq (plist-get node :ebox-type)
|
||||
'(box concat stack flex flex-item))
|
||||
'(box concat stack flex))
|
||||
(setq supported nil)))
|
||||
nodes))
|
||||
supported))
|
||||
@ -482,7 +540,7 @@ type, key, parent, and sibling position are identical."
|
||||
Return nil without mutating STATE when the native capability proof is absent."
|
||||
(when (and (require 'ebox-native-reflow nil t)
|
||||
(ebox-native-reflow-layout-ready-p)
|
||||
(memq (plist-get state :projection-kind) '(nil native-frame))
|
||||
(eq (plist-get state :projection-kind) 'native-frame)
|
||||
(ebox-native-commit--runtime-types-supported-p state)
|
||||
(ebox-native-commit--region-index-safe-p state))
|
||||
(when-let* ((frame-spec (ebox-native-commit--frame-spec state node)))
|
||||
|
||||
@ -46,6 +46,13 @@
|
||||
(declare-function ebox-native--module-release "ebox_native_reflow" (session))
|
||||
(declare-function ebox--grapheme-clusters "ebox-layout" (string))
|
||||
(declare-function ebox--format-content-string "ebox-layout" (box content))
|
||||
(declare-function ebox-style-no-soft-wrap-p "ebox-style" (value))
|
||||
(declare-function ebox-style-soft-wrap-p "ebox-style" (value))
|
||||
(declare-function ebox-style--text-paint-owner-p "ebox-style" (node))
|
||||
(declare-function ebox-layout-config-kind "ebox-layout-config" (config))
|
||||
(declare-function ebox-layout-config-props "ebox-layout-config" (config))
|
||||
(declare-function ebox-tree-node-children "ebox-tree" (node))
|
||||
(declare-function ebox-buffer--font-face "ebox-buffer-backend" (style))
|
||||
(declare-function ebox--ensure-node-id "ebox" (node))
|
||||
(declare-function ebox--buffer-render-state "ebox-incremental" (buffer))
|
||||
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||
@ -55,7 +62,6 @@
|
||||
(declare-function ebox--flex-container-content-props
|
||||
"ebox-flex" (props raw-props wrapper-box))
|
||||
(declare-function ebox--flex-item-props "ebox-flex" (item))
|
||||
(declare-function ebox--flex-item-source-node "ebox-tree" (item))
|
||||
(declare-function ebox--flex-line-value "ebox-flex" (value &optional default))
|
||||
(declare-function ebox--content-min-pixel "ebox-layout" (box))
|
||||
(declare-function ebox--horizontal-size-pixels
|
||||
@ -1308,6 +1314,29 @@ Ebox nodes, identities, text, properties, or publication state."
|
||||
:layout-fragment-cache (make-hash-table :test 'equal)
|
||||
:layout-fragment-revision 0)))
|
||||
|
||||
(cl-defun ebox-native-reflow-fork-session
|
||||
(session &key (workers 1) (max-jobs 4) (max-results 4)
|
||||
(max-result-bytes ebox-native-reflow-max-result-bytes))
|
||||
"Return an unpublished native child of committed SESSION.
|
||||
The child owns a fresh Rust handle, while immutable compiled layout facts and
|
||||
a private copy of the fragment index seed its first frame. Pending/confirmed
|
||||
frame state deliberately stays with SESSION because it names that handle."
|
||||
(ebox-native-reflow--live-handle session)
|
||||
(let ((fork
|
||||
(ebox-native-reflow-create-session
|
||||
:workers workers :max-jobs max-jobs :max-results max-results
|
||||
:max-result-bytes max-result-bytes)))
|
||||
(setf (ebox-native-reflow-session-styles fork)
|
||||
(copy-sequence (ebox-native-reflow-session-styles session))
|
||||
(ebox-native-reflow-session-layout-package fork)
|
||||
(ebox-native-reflow-session-layout-package session)
|
||||
(ebox-native-reflow-session-layout-fragment-cache fork)
|
||||
(copy-hash-table
|
||||
(ebox-native-reflow-session-layout-fragment-cache session))
|
||||
(ebox-native-reflow-session-layout-fragment-revision fork)
|
||||
(ebox-native-reflow-session-layout-fragment-revision session))
|
||||
fork))
|
||||
|
||||
(defun ebox-native-reflow--live-handle (session)
|
||||
"Return SESSION's live module handle or signal an error."
|
||||
(unless (and (ebox-native-reflow-session-p session)
|
||||
@ -1599,7 +1628,7 @@ This mirrors the static half of `ebox--render-transparent-preformatted-box'.
|
||||
Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
|
||||
(and (eq box ebox-native-reflow--compile-root-node)
|
||||
(eq (plist-get source-child :ebox-type) 'stack)
|
||||
(null (ebox-get box :wrap-mode))
|
||||
(ebox-style-no-soft-wrap-p (ebox-get box :wrap-mode))
|
||||
(eq (ebox-get box :text-align) 'left)
|
||||
(eq (ebox-get box :vertical-align) 'top)
|
||||
(null (ebox-get box :height))
|
||||
@ -1618,16 +1647,145 @@ Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
|
||||
(null (ebox-get box :color))
|
||||
(null (ebox-get box :bgcolor))))
|
||||
|
||||
(defun ebox-native-reflow--simple-axis-align (value)
|
||||
"Return Flex IR alignment equivalent to simple-axis VALUE."
|
||||
(pcase value
|
||||
('start "flex-start")
|
||||
('end "flex-end")
|
||||
((or 'center 'stretch) (symbol-name value))
|
||||
(_ (error "Native reflow unsupported Row/Column alignment: %S" value))))
|
||||
|
||||
(defun ebox-native-reflow--simple-axis-item-align (kind child cross-align)
|
||||
"Return CHILD alignment for simple-axis KIND and CROSS-ALIGN."
|
||||
(cond
|
||||
((eq (plist-get child :ebox-kind) 'text) "flex-start")
|
||||
((not (eq cross-align 'stretch))
|
||||
(ebox-native-reflow--simple-axis-align cross-align))
|
||||
(t
|
||||
(let ((cross-size (ebox-get child (if (eq kind 'row) :height :width))))
|
||||
(if (memq cross-size '(nil auto stretch))
|
||||
"stretch"
|
||||
"flex-start")))))
|
||||
|
||||
(defun ebox-native-reflow--compile-typed-axis-children
|
||||
(kind config children &optional fragments)
|
||||
"Compile canonical Row/Column CHILDREN for KIND and CONFIG.
|
||||
The native IR represents this simple-axis layout as fixed Flex items so
|
||||
cross-axis stretch and item gap remain explicit without legacy runtime nodes."
|
||||
(let* ((props (ebox-layout-config-props config))
|
||||
(gap (plist-get props :item-gap))
|
||||
(cross-align (plist-get props :cross-align))
|
||||
(row-p (eq kind 'row))
|
||||
(compiled-children
|
||||
(mapcar
|
||||
(lambda (child)
|
||||
(if fragments
|
||||
(ebox-native-reflow--compiled-scene-child fragments child)
|
||||
(ebox-native-reflow--compile-node child)))
|
||||
children)))
|
||||
(if (and (memq kind '(row column))
|
||||
(equal gap 0) (eq cross-align 'stretch))
|
||||
;; Default Row/Column are exactly Rust's simple axis algorithms:
|
||||
;; children are top-aligned/padded on Row and share the known content
|
||||
;; viewport on Column. Sending this subset through Flex repeats
|
||||
;; intrinsic measurement and distribution at every nesting level.
|
||||
(list :type (symbol-name kind)
|
||||
:children (vconcat compiled-children))
|
||||
(list
|
||||
:type "flex"
|
||||
:direction (symbol-name kind)
|
||||
:wrap "nowrap"
|
||||
:justify "flex-start"
|
||||
:align-items
|
||||
(ebox-native-reflow--simple-axis-align
|
||||
(plist-get props :cross-align))
|
||||
:align-content "stretch"
|
||||
;; Row/Column are the complete child layout of their owning Box. Their
|
||||
;; inline axis therefore uses that Box's content viewport; `auto' would
|
||||
;; collapse a Column to max-content and lose cross-axis stretch.
|
||||
:width (list :kind "stretch")
|
||||
:height (list :kind "auto")
|
||||
:row-gap (if row-p 0 gap)
|
||||
:column-gap (if row-p gap 0)
|
||||
:items
|
||||
(vconcat
|
||||
(cl-mapcar
|
||||
(lambda (child compiled)
|
||||
(list :node compiled
|
||||
:order 0 :grow 0 :shrink 0
|
||||
:basis (list :kind "auto")
|
||||
:align-self
|
||||
(ebox-native-reflow--simple-axis-item-align
|
||||
kind child cross-align)))
|
||||
children compiled-children))))))
|
||||
|
||||
(defun ebox-native-reflow--compile-typed-box-child (box)
|
||||
"Compile canonical BOX's typed child layout, or return nil."
|
||||
(let* ((config (plist-get box :ebox-layout-config))
|
||||
(kind (and config (ebox-layout-config-kind config)))
|
||||
(children (ebox-tree-node-children box)))
|
||||
(unless config
|
||||
(error "Native reflow canonical Box has no LayoutConfig"))
|
||||
(pcase kind
|
||||
('normal
|
||||
(pcase children
|
||||
('() nil)
|
||||
(`(,child) (ebox-native-reflow--compile-node child))
|
||||
(_ (error "Native reflow does not yet compile multi-child Normal Box"))))
|
||||
((or 'row 'column)
|
||||
(ebox-native-reflow--compile-typed-axis-children
|
||||
kind config children))
|
||||
('flex
|
||||
(ebox-native-reflow--compile-flex-inner
|
||||
(copy-sequence (ebox-layout-config-props config)) children))
|
||||
('grid
|
||||
(error "Native reflow does not yet compile Grid LayoutConfig"))
|
||||
(_ (error "Native reflow unsupported LayoutConfig: %S" kind)))))
|
||||
|
||||
(defun ebox-native-reflow--compile-text-node (node)
|
||||
"Compile canonical Text NODE to the lightweight native Text IR."
|
||||
(let* ((region-id (ebox--ensure-region-id node))
|
||||
(typography-style
|
||||
(ebox-native-reflow--compile-add-face-style
|
||||
(ebox-buffer--font-face node)))
|
||||
(foreground-style
|
||||
(ebox-native-reflow--compile-add-face-style
|
||||
(ebox-native-reflow--compile-foreground-face
|
||||
(ebox-get node :color))))
|
||||
(surface-template-id
|
||||
(ebox-native-reflow--register-property-template
|
||||
(ebox-get node :surface-properties) "text surface"))
|
||||
(wrap-mode (or (ebox-get node :wrap-mode) 'none)))
|
||||
(list :type "text"
|
||||
:region-id region-id
|
||||
:content
|
||||
(ebox-native-reflow--compile-text
|
||||
(or (plist-get node :ebox-text-value)
|
||||
(ebox-get node :content)
|
||||
""))
|
||||
:typography-style (or typography-style :null)
|
||||
:foreground-style (or foreground-style :null)
|
||||
:surface-template-id (or surface-template-id :null)
|
||||
:wrap-mode
|
||||
(ebox-native-reflow--symbol-name
|
||||
wrap-mode 'none '(none word char) "Text wrap-mode"))))
|
||||
|
||||
(defun ebox-native-reflow--compile-box
|
||||
(box &optional child-override child-override-p child-resolved-p)
|
||||
"Compile normalized Ebox BOX to strict layout IR."
|
||||
(when (plist-member box :children)
|
||||
(error "Native reflow does not yet compile typed Box children"))
|
||||
(let* ((source-child (plist-get box :ebox-content-node))
|
||||
(let* ((typed-box-p (eq (plist-get box :ebox-kind) 'box))
|
||||
(typed-text-p (eq (plist-get box :ebox-kind) 'text))
|
||||
(typed-layout-kind
|
||||
(and typed-box-p
|
||||
(ebox-layout-config-kind
|
||||
(plist-get box :ebox-layout-config))))
|
||||
(source-child (plist-get box :ebox-content-node))
|
||||
(child (if (or child-override-p child-resolved-p)
|
||||
child-override
|
||||
(and source-child
|
||||
(ebox-native-reflow--compile-node source-child))))
|
||||
(if typed-box-p
|
||||
(ebox-native-reflow--compile-typed-box-child box)
|
||||
(and source-child
|
||||
(ebox-native-reflow--compile-node source-child)))))
|
||||
(wrap-mode (or (ebox-get box :wrap-mode) 'none))
|
||||
(kp-content
|
||||
(and (not child)
|
||||
@ -1647,7 +1805,12 @@ Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
|
||||
(foreground-style
|
||||
(ebox-native-reflow--compile-add-face-style
|
||||
(ebox-native-reflow--compile-foreground-face
|
||||
(ebox-get box :color))))
|
||||
(and (ebox-style--text-paint-owner-p box)
|
||||
(ebox-get box :color)))))
|
||||
(typography-style
|
||||
(and (ebox-style--text-paint-owner-p box)
|
||||
(ebox-native-reflow--compile-add-face-style
|
||||
(ebox-buffer--font-face box))))
|
||||
(background-style
|
||||
(ebox-native-reflow--compile-add-face-style
|
||||
(let ((background (ebox-get box :bgcolor)))
|
||||
@ -1679,24 +1842,40 @@ Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
|
||||
(when color (list :color color)))))))))
|
||||
(surface-template-id
|
||||
(ebox-native-reflow--register-property-template
|
||||
(ebox-get box :surface-properties) "surface")))
|
||||
(ebox-get box :surface-properties) "surface"))
|
||||
(fused-text-p
|
||||
(and typed-box-p
|
||||
(eq typed-layout-kind 'normal)
|
||||
(equal (plist-get child :type) "text"))))
|
||||
(list
|
||||
:type "box"
|
||||
:region-id region-id
|
||||
:content (if child
|
||||
:null
|
||||
:content (cond
|
||||
(fused-text-p (plist-get child :content))
|
||||
(child :null)
|
||||
(t
|
||||
(ebox-native-reflow--compile-text
|
||||
(or kp-content (ebox-get box :content) "")
|
||||
(and kp-content t)))
|
||||
:child (or child :null)
|
||||
(and kp-content t))))
|
||||
:content-region-id
|
||||
(if fused-text-p (plist-get child :region-id) :null)
|
||||
:content-typography-style
|
||||
(if fused-text-p (plist-get child :typography-style) :null)
|
||||
:content-foreground-style
|
||||
(if fused-text-p (plist-get child :foreground-style) :null)
|
||||
:content-surface-template-id
|
||||
(if fused-text-p (plist-get child :surface-template-id) :null)
|
||||
:child (if fused-text-p :null (or child :null))
|
||||
:content-width-exact
|
||||
(if (or child-override-p
|
||||
(memq typed-layout-kind '(row column flex grid))
|
||||
(ebox-native-reflow--transparent-stack-content-exact-p
|
||||
box source-child))
|
||||
t
|
||||
:false)
|
||||
:width (ebox-native-reflow--compile-size
|
||||
(ebox-get box :width) 'horizontal "auto")
|
||||
(if typed-text-p 'max-content (ebox-get box :width))
|
||||
'horizontal "auto")
|
||||
:min-width (ebox-native-reflow--compile-size
|
||||
(ebox-get box :min-width) 'horizontal "pixels")
|
||||
:max-width (ebox-native-reflow--compile-size
|
||||
@ -1721,6 +1900,7 @@ Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
|
||||
:margin-bottom margin-bottom
|
||||
:border-left border-left
|
||||
:border-right border-right
|
||||
:typography-style (or typography-style :null)
|
||||
:foreground-style (or foreground-style :null)
|
||||
:background-style (or background-style :null)
|
||||
:border-left-style (or border-left-style :null)
|
||||
@ -1757,7 +1937,7 @@ Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
|
||||
(item axis &optional compiled-node compiled-node-p
|
||||
content-min-width content-min-width-p)
|
||||
"Compile flex ITEM metadata and source for AXIS."
|
||||
(let* ((source (ebox--flex-item-source-node item))
|
||||
(let* ((source item)
|
||||
(props (ebox--flex-item-props item))
|
||||
(order (plist-get props :order))
|
||||
(grow (plist-get props :flex-grow))
|
||||
@ -1785,12 +1965,14 @@ Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
|
||||
;; the source subtree again during native package construction.
|
||||
(when (and (eq axis 'row)
|
||||
(eq (plist-get source :ebox-type) 'box)
|
||||
(ebox-get source :wrap-mode)
|
||||
;; A composite child already exists in the compiled fragment;
|
||||
;; Rust derives its intrinsic minimum directly from that child
|
||||
;; when the visible Elisp render did not measure it yet.
|
||||
(or content-min-width-p
|
||||
(null (plist-get source :ebox-content-node))))
|
||||
(not (eq (plist-get source :ebox-kind) 'text))
|
||||
(ebox-style-soft-wrap-p (ebox-get source :wrap-mode))
|
||||
;; A composite child already exists in the compiled fragment;
|
||||
;; Rust derives its intrinsic minimum directly from that child
|
||||
;; when the visible Elisp render did not measure it yet.
|
||||
(or content-min-width-p
|
||||
(and (null (plist-get source :ebox-content-node))
|
||||
(null (ebox-tree-node-children source)))))
|
||||
(unless content-min-width-p
|
||||
(setq content-min-width (ebox--content-min-pixel source)))
|
||||
(setq compiled
|
||||
@ -1806,88 +1988,89 @@ Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
|
||||
(if (eq axis 'row) 'horizontal 'vertical)
|
||||
"auto")
|
||||
:align-self
|
||||
(ebox-native-reflow--compile-flex-align
|
||||
(plist-get props :align-self) 'auto "align-self"))))
|
||||
(if (eq (plist-get source :ebox-kind) 'text)
|
||||
"flex-start"
|
||||
(ebox-native-reflow--compile-flex-align
|
||||
(plist-get props :align-self) 'auto "align-self")))))
|
||||
|
||||
(defun ebox-native-reflow--compile-flex-inner
|
||||
(props items &optional fragments flex-content-min-widths)
|
||||
"Compile Flex PROPS and ITEMS to one backend-neutral native IR node."
|
||||
(let ((axis (ebox--flex-axis props)))
|
||||
(list
|
||||
:type "flex"
|
||||
:direction
|
||||
(ebox-native-reflow--symbol-name
|
||||
(plist-get props :flex-direction) 'row
|
||||
'(row row-reverse column column-reverse) "flex-direction")
|
||||
:wrap
|
||||
(ebox-native-reflow--symbol-name
|
||||
(plist-get props :flex-wrap) 'nowrap
|
||||
'(nowrap wrap wrap-reverse) "flex-wrap")
|
||||
:justify
|
||||
(ebox-native-reflow--compile-flex-align
|
||||
(plist-get props :justify-content) 'flex-start "justify-content")
|
||||
:align-items
|
||||
(ebox-native-reflow--compile-flex-align
|
||||
(plist-get props :align-items) 'stretch "align-items")
|
||||
:align-content
|
||||
(ebox-native-reflow--compile-flex-align
|
||||
(plist-get props :align-content) 'stretch "align-content")
|
||||
:width
|
||||
(ebox-native-reflow--compile-size
|
||||
(plist-get props :width) 'horizontal "auto")
|
||||
:height
|
||||
(ebox-native-reflow--compile-size
|
||||
(plist-get props :height) 'vertical "auto")
|
||||
:row-gap (or (ebox--flex-line-value (plist-get props :row-gap) 0) 0)
|
||||
:column-gap
|
||||
(or (ebox--horizontal-size-pixels (plist-get props :column-gap) 0) 0)
|
||||
:items
|
||||
(vconcat
|
||||
(mapcar
|
||||
(lambda (item)
|
||||
(if fragments
|
||||
(let* ((source item)
|
||||
(missing (make-symbol "missing-content-min"))
|
||||
(content-min
|
||||
(if flex-content-min-widths
|
||||
(gethash source flex-content-min-widths missing)
|
||||
missing)))
|
||||
(ebox-native-reflow--compile-flex-item
|
||||
item axis
|
||||
(ebox-native-reflow--compiled-scene-child fragments source)
|
||||
t content-min (not (eq content-min missing))))
|
||||
(ebox-native-reflow--compile-flex-item item axis)))
|
||||
items)))))
|
||||
|
||||
(defun ebox-native-reflow--compile-flex
|
||||
(node &optional fragments flex-content-min-widths)
|
||||
"Compile normalized flex NODE, translating its wrapper to a box node."
|
||||
"Compile normalized legacy flex NODE through the shared Flex IR builder."
|
||||
(let* ((wrapper (plist-get node :box))
|
||||
(props (ebox--flex-container-content-props
|
||||
(plist-get node :props)
|
||||
(plist-get node :raw-props)
|
||||
wrapper))
|
||||
(_dynamic-wrapper-size
|
||||
(when wrapper
|
||||
;; The surrounding native box supplies its resolved content width
|
||||
;; to the child for each frame. Do not freeze the compiler's
|
||||
;; ambient viewport into a multi-frame layout document.
|
||||
(plist-put props :width nil)
|
||||
(when (plist-member (plist-get node :raw-props) :height)
|
||||
(plist-put props :height 'viewport-height))))
|
||||
(axis (ebox--flex-axis props))
|
||||
(inner
|
||||
(list
|
||||
:type "flex"
|
||||
:direction
|
||||
(ebox-native-reflow--symbol-name
|
||||
(plist-get props :flex-direction) 'row
|
||||
'(row row-reverse column column-reverse) "flex-direction")
|
||||
:wrap
|
||||
(ebox-native-reflow--symbol-name
|
||||
(plist-get props :flex-wrap) 'nowrap
|
||||
'(nowrap wrap wrap-reverse) "flex-wrap")
|
||||
:justify
|
||||
(ebox-native-reflow--compile-flex-align
|
||||
(plist-get props :justify-content) 'flex-start
|
||||
"justify-content")
|
||||
:align-items
|
||||
(ebox-native-reflow--compile-flex-align
|
||||
(plist-get props :align-items) 'stretch "align-items")
|
||||
:align-content
|
||||
(ebox-native-reflow--compile-flex-align
|
||||
(plist-get props :align-content) 'stretch "align-content")
|
||||
:width
|
||||
(ebox-native-reflow--compile-size
|
||||
(plist-get props :width) 'horizontal "auto")
|
||||
:height
|
||||
(ebox-native-reflow--compile-size
|
||||
(plist-get props :height) 'vertical "auto")
|
||||
:row-gap
|
||||
(or (ebox--flex-line-value (plist-get props :row-gap) 0) 0)
|
||||
:column-gap
|
||||
(or (ebox--horizontal-size-pixels
|
||||
(plist-get props :column-gap) 0)
|
||||
0)
|
||||
:items
|
||||
(vconcat
|
||||
(mapcar (lambda (item)
|
||||
(if fragments
|
||||
(let* ((source (ebox--flex-item-source-node item))
|
||||
(missing (make-symbol "missing-content-min"))
|
||||
(content-min
|
||||
(if flex-content-min-widths
|
||||
(gethash source
|
||||
flex-content-min-widths
|
||||
missing)
|
||||
missing)))
|
||||
(ebox-native-reflow--compile-flex-item
|
||||
item axis
|
||||
(ebox-native-reflow--compiled-scene-child
|
||||
fragments source)
|
||||
t
|
||||
content-min
|
||||
(not (eq content-min missing))))
|
||||
(ebox-native-reflow--compile-flex-item item axis)))
|
||||
(ebox-tree-layout-children node))))))
|
||||
(if wrapper
|
||||
(ebox-native-reflow--compile-box wrapper inner t)
|
||||
inner)))
|
||||
wrapper)))
|
||||
(when wrapper
|
||||
;; The surrounding native box supplies its resolved content width to the
|
||||
;; child for each frame. Do not freeze the compiler's ambient viewport.
|
||||
(plist-put props :width nil)
|
||||
(when (plist-member (plist-get node :raw-props) :height)
|
||||
(plist-put props :height 'viewport-height)))
|
||||
(let ((inner
|
||||
(ebox-native-reflow--compile-flex-inner
|
||||
props (ebox-tree-layout-children node)
|
||||
fragments flex-content-min-widths)))
|
||||
(if wrapper
|
||||
(ebox-native-reflow--compile-box wrapper inner t)
|
||||
inner))))
|
||||
|
||||
(defun ebox-native-reflow--compile-node (node)
|
||||
"Compile supported normalized Ebox NODE to strict layout IR."
|
||||
(pcase (and (listp node) (plist-get node :ebox-type))
|
||||
('box (ebox-native-reflow--compile-box node))
|
||||
('box (if (eq (plist-get node :ebox-kind) 'text)
|
||||
(ebox-native-reflow--compile-text-node node)
|
||||
(ebox-native-reflow--compile-box node)))
|
||||
('concat
|
||||
(list :type "row"
|
||||
:children
|
||||
@ -1899,7 +2082,6 @@ Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
|
||||
(vconcat (mapcar #'ebox-native-reflow--compile-node
|
||||
(ebox--layout-children node)))))
|
||||
('flex (ebox-native-reflow--compile-flex node))
|
||||
('flex-item (ebox-native-reflow--compile-node (plist-get node :node)))
|
||||
(_ (error "Native reflow unsupported node type: %S"
|
||||
(and (listp node) (plist-get node :ebox-type))))))
|
||||
|
||||
@ -1912,9 +2094,21 @@ backend has a matching two-dimensional layout contract."
|
||||
((plist-get node :ebox-child-sequence) nil)
|
||||
((eq (plist-get node :ebox-type) 'grid) nil)
|
||||
((eq (plist-get node :ebox-type) 'box)
|
||||
(and (not (plist-member node :children))
|
||||
(ebox-native-reflow--native-node-supported-p
|
||||
(plist-get node :ebox-content-node))))
|
||||
(if-let* ((config (and (eq (plist-get node :ebox-kind) 'box)
|
||||
(plist-get node :ebox-layout-config))))
|
||||
(let ((kind (ebox-layout-config-kind config))
|
||||
(children (ebox-tree-node-children node)))
|
||||
(and
|
||||
(pcase kind
|
||||
('normal (<= (length children) 1))
|
||||
((or 'row 'column)
|
||||
t)
|
||||
('flex t)
|
||||
(_ nil))
|
||||
(cl-every #'ebox-native-reflow--native-node-supported-p children)))
|
||||
(and (not (plist-member node :children))
|
||||
(ebox-native-reflow--native-node-supported-p
|
||||
(plist-get node :ebox-content-node)))))
|
||||
((memq (plist-get node :ebox-type) '(concat stack))
|
||||
(cl-every #'ebox-native-reflow--native-node-supported-p
|
||||
(ebox--layout-children node)))
|
||||
@ -1923,16 +2117,40 @@ backend has a matching two-dimensional layout contract."
|
||||
(append (when-let* ((box (plist-get node :box)))
|
||||
(list box))
|
||||
(ebox-tree-layout-children node))))
|
||||
((eq (plist-get node :ebox-type) 'flex-item)
|
||||
(ebox-native-reflow--native-node-supported-p (plist-get node :node)))
|
||||
(t nil)))
|
||||
|
||||
(defun ebox-native-reflow--compile-typed-scene-child
|
||||
(box fragments flex-content-min-widths)
|
||||
"Compile typed BOX child layout from already compiled FRAGMENTS."
|
||||
(let* ((config (plist-get box :ebox-layout-config))
|
||||
(kind (ebox-layout-config-kind config))
|
||||
(children (ebox-tree-node-children box)))
|
||||
(pcase kind
|
||||
('normal
|
||||
(and children
|
||||
(ebox-native-reflow--compiled-scene-child
|
||||
fragments (car children))))
|
||||
((or 'row 'column)
|
||||
(ebox-native-reflow--compile-typed-axis-children
|
||||
kind config children fragments))
|
||||
('flex
|
||||
(ebox-native-reflow--compile-flex-inner
|
||||
(copy-sequence (ebox-layout-config-props config))
|
||||
children fragments flex-content-min-widths))
|
||||
(_ (error "Native reflow unsupported typed scene LayoutConfig: %S"
|
||||
kind)))))
|
||||
|
||||
(defun ebox-native-reflow--compiled-scene-child (fragments node)
|
||||
"Return NODE's compiled fragment from FRAGMENTS, failing closed."
|
||||
(let ((missing (make-symbol "missing-native-scene-fragment")))
|
||||
(let ((fragment (gethash node fragments missing)))
|
||||
(when (eq fragment missing)
|
||||
(error "Native render scene is missing a child fragment"))
|
||||
(error "Native render scene is missing child node %S (%S/%S)"
|
||||
(plist-get node :node-id)
|
||||
(plist-get node :ebox-kind)
|
||||
(and (plist-get node :ebox-layout-config)
|
||||
(ebox-layout-config-kind
|
||||
(plist-get node :ebox-layout-config)))))
|
||||
fragment)))
|
||||
|
||||
(defun ebox-native-reflow--compile-scene-node
|
||||
@ -1940,12 +2158,20 @@ backend has a matching two-dimensional layout contract."
|
||||
"Compile one flat scene NODE from already compiled FRAGMENTS."
|
||||
(pcase (and (listp node) (plist-get node :ebox-type))
|
||||
('box
|
||||
(let ((child (plist-get node :ebox-content-node)))
|
||||
(ebox-native-reflow--compile-box
|
||||
node
|
||||
(and child
|
||||
(ebox-native-reflow--compiled-scene-child fragments child))
|
||||
nil t)))
|
||||
(if (eq (plist-get node :ebox-kind) 'text)
|
||||
(ebox-native-reflow--compile-text-node node)
|
||||
(if (eq (plist-get node :ebox-kind) 'box)
|
||||
(ebox-native-reflow--compile-box
|
||||
node
|
||||
(ebox-native-reflow--compile-typed-scene-child
|
||||
node fragments flex-content-min-widths)
|
||||
nil t)
|
||||
(let ((child (plist-get node :ebox-content-node)))
|
||||
(ebox-native-reflow--compile-box
|
||||
node
|
||||
(and child
|
||||
(ebox-native-reflow--compiled-scene-child fragments child))
|
||||
nil t)))))
|
||||
('concat
|
||||
(list :type "row"
|
||||
:children
|
||||
@ -1965,9 +2191,6 @@ backend has a matching two-dimensional layout contract."
|
||||
('flex
|
||||
(ebox-native-reflow--compile-flex
|
||||
node fragments flex-content-min-widths))
|
||||
('flex-item
|
||||
(ebox-native-reflow--compiled-scene-child
|
||||
fragments (plist-get node :node)))
|
||||
(_ (error "Native reflow unsupported scene node type: %S"
|
||||
(and (listp node) (plist-get node :ebox-type))))))
|
||||
|
||||
@ -2153,12 +2376,13 @@ per call, and no call recursively visits the captured Ebox tree."
|
||||
(defun ebox-native-reflow--retained-layout-children (node)
|
||||
"Return NODE children referenced by one compiled native fragment."
|
||||
(pcase (plist-get node :ebox-type)
|
||||
('box (when-let* ((child (plist-get node :ebox-content-node)))
|
||||
(list child)))
|
||||
('box
|
||||
(if (eq (plist-get node :ebox-kind) 'box)
|
||||
(ebox-tree-node-children node)
|
||||
(when-let* ((child (plist-get node :ebox-content-node)))
|
||||
(list child))))
|
||||
((or 'concat 'stack) (ebox--layout-children node))
|
||||
('flex
|
||||
(mapcar #'ebox--flex-item-source-node
|
||||
(ebox-tree-layout-children node)))
|
||||
('flex (ebox-tree-layout-children node))
|
||||
(_ nil)))
|
||||
|
||||
(defun ebox-native-reflow--retained-layout-postorder (root)
|
||||
@ -2169,8 +2393,7 @@ per call, and no call recursively visits the captured Ebox tree."
|
||||
(dolist (child
|
||||
(ebox-native-reflow--retained-layout-children node))
|
||||
(visit child))
|
||||
(unless (eq (plist-get node :ebox-type) 'flex-item)
|
||||
(push node nodes))))
|
||||
(push node nodes)))
|
||||
(visit root))
|
||||
(vconcat (nreverse nodes))))
|
||||
|
||||
@ -2214,7 +2437,8 @@ per call, and no call recursively visits the captured Ebox tree."
|
||||
(touched-ids (plist-get state :native-touched-node-ids))
|
||||
(touched-set
|
||||
(and (plist-get state :native-topology-stable-p)
|
||||
(consp touched-ids)
|
||||
(plist-member state :native-touched-node-ids)
|
||||
(proper-list-p touched-ids)
|
||||
(make-hash-table :test 'equal)))
|
||||
(retained-fast-p
|
||||
(and touched-set
|
||||
@ -3646,8 +3870,11 @@ metadata together with the text."
|
||||
(let ((style (aref styles style-id)))
|
||||
(pcase (plist-get style :mode)
|
||||
('set (setq faces (list (plist-get style :face))))
|
||||
('add (setq faces
|
||||
(append faces (list (plist-get style :face)))))
|
||||
('add
|
||||
(let ((face (plist-get style :face)))
|
||||
(unless (and (= (length faces) 1)
|
||||
(equal (car faces) face))
|
||||
(setq faces (append faces (list face))))))
|
||||
(_ (error "Native reflow frame has an invalid style mode")))))))
|
||||
|
||||
(defun ebox-native-reflow--fragment-face-baseline
|
||||
|
||||
@ -31,6 +31,29 @@
|
||||
:wrap-mode word :scroll-offset 0)
|
||||
"Default longhand fields for the current runtime box encoding.")
|
||||
|
||||
(defconst ebox-node-factory--text-longhand
|
||||
'(:content ""
|
||||
:overflow visible
|
||||
:wrap-mode word)
|
||||
"Minimal runtime fields owned by canonical Text.")
|
||||
|
||||
(defconst ebox-node-factory--box-longhand
|
||||
'(:content ""
|
||||
:box-sizing border-box
|
||||
:min-width 0 :min-height 0
|
||||
:padding-left-pixel 0 :padding-right-pixel 0
|
||||
:padding-top-height 0 :padding-bottom-height 0
|
||||
:margin-left-pixel 0 :margin-right-pixel 0
|
||||
:margin-top-height 0 :margin-bottom-height 0
|
||||
:border-left-pixel 0 :border-right-pixel 0
|
||||
:text-align left :vertical-align top
|
||||
:overflow scroll :visibility visible
|
||||
:wrap-mode word :scroll-offset 0)
|
||||
"Runtime-required defaults owned by canonical Box.
|
||||
Nil paint and optional sizing fields remain sparse. Numeric frame insets are
|
||||
materialized because every layout algorithm consumes them as definite values;
|
||||
leaving them absent would make the canonical node violate the Box contract.")
|
||||
|
||||
(defun ebox--longhand-create (&optional plist)
|
||||
"Return a runtime box field plist overridden by PLIST."
|
||||
(let ((box (copy-sequence ebox--longhand)))
|
||||
@ -40,23 +63,42 @@
|
||||
(plist-put box key value)))
|
||||
box))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-create (&rest plist)
|
||||
"Create one legacy runtime box from PLIST.
|
||||
|
||||
New framework integrations should use `ebox-text-create' and
|
||||
`ebox-box-create'. This factory remains public during source-tree migration."
|
||||
(let ((declarations (ebox-style-compile-declarations plist))
|
||||
(expanded (ebox-style-expand-ebox-plist plist))
|
||||
(controlled-scroll-offset-p (plist-member plist :scroll-offset)))
|
||||
(let ((box (ebox--longhand-create expanded)))
|
||||
(defun ebox-node-factory--create-expanded (expanded declarations)
|
||||
"Create one runtime box from EXPANDED engine facts and DECLARATIONS."
|
||||
(let ((controlled-scroll-offset-p
|
||||
(plist-member expanded :scroll-offset)))
|
||||
(let ((box (copy-sequence ebox-node-factory--box-longhand)))
|
||||
(cl-loop for (property value) on expanded by #'cddr
|
||||
do (plist-put box property value))
|
||||
(plist-put box :ebox-type 'box)
|
||||
(plist-put box :display '(block flow))
|
||||
(plist-put box :ebox-style-declarations declarations)
|
||||
(plist-put box :ebox-style-declarations (copy-sequence declarations))
|
||||
(when controlled-scroll-offset-p
|
||||
(plist-put box :ebox-scroll-offset-controlled-p t))
|
||||
box)))
|
||||
|
||||
(defun ebox-node-factory--create-text-expanded (expanded declarations)
|
||||
"Create canonical Text storage from EXPANDED facts and DECLARATIONS."
|
||||
(let ((node (copy-sequence ebox-node-factory--text-longhand)))
|
||||
(cl-loop for (property value) on expanded by #'cddr
|
||||
do (plist-put node property value))
|
||||
(plist-put node :ebox-type 'box)
|
||||
(plist-put node :display '(inline flow))
|
||||
(plist-put node :ebox-style-declarations (copy-sequence declarations))
|
||||
node))
|
||||
|
||||
(defun ebox-node-factory--create (plist declarations)
|
||||
"Create one runtime box from normalized PLIST and DECLARATIONS.
|
||||
PLIST contains already-validated public engine inputs; DECLARATIONS are the
|
||||
single canonical author fact set retained for style projection."
|
||||
(ebox-node-factory--create-expanded
|
||||
(ebox-style-expand-ebox-plist plist) declarations))
|
||||
|
||||
(defun ebox-node-factory--create-from-properties (&rest plist)
|
||||
"Create one private runtime box from evaluated engine PLIST."
|
||||
(ebox-node-factory--create
|
||||
plist (ebox-style-compile-declarations plist)))
|
||||
|
||||
(provide 'ebox-node-factory)
|
||||
|
||||
;;; ebox-node-factory.el ends here
|
||||
|
||||
@ -11,6 +11,8 @@
|
||||
(defvar ebox-viewport-height nil)
|
||||
(defvar ebox--render-runtime-revision nil)
|
||||
(defvar ebox--surface-materialization-active nil)
|
||||
(defvar ebox--render-root-parent-kind nil
|
||||
"Formatting context owning the detached render root, or nil.")
|
||||
(defvar ebox--render-cache-table nil)
|
||||
(defvar ebox--render-cache-signature-cache nil)
|
||||
(defvar ebox--viewport-dependent-node-ids-cache nil)
|
||||
|
||||
@ -111,8 +111,6 @@
|
||||
('grid
|
||||
(when-let* ((box (plist-get node :box)))
|
||||
(ebox--ensure-region-id box)))
|
||||
('flex-item
|
||||
(ebox-selector--node-region-id (plist-get node :node)))
|
||||
(_ nil)))
|
||||
|
||||
;;;###autoload
|
||||
|
||||
269
ebox-style.el
269
ebox-style.el
@ -13,7 +13,8 @@
|
||||
|
||||
(defvar ebox--longhand)
|
||||
(declare-function ebox-selector-parse "ebox-selector" (selector))
|
||||
(declare-function ebox-create "ebox" (&rest plist))
|
||||
(declare-function ebox-node-factory--create-from-properties
|
||||
"ebox-node-factory" (&rest plist))
|
||||
(declare-function ebox--flex-normalize-container-props
|
||||
"ebox-flex" (props))
|
||||
(declare-function ebox-grid--normalize-config-props
|
||||
@ -156,8 +157,10 @@
|
||||
:normalizer vertical-align)
|
||||
(:name :overflow :id ebox/overflow :initial scroll :contexts (box)
|
||||
:group structure :dirty-kind geometry :signature layout)
|
||||
(:name :wrap-mode :id ebox/wrap-mode :initial word :contexts (box)
|
||||
:group geometry :dirty-kind geometry :signature layout)
|
||||
(:name :wrap-mode :id ebox/wrap-mode :initial word :inherits t
|
||||
:contexts (box)
|
||||
:group geometry :dirty-kind geometry :signature layout
|
||||
:validator wrap-mode)
|
||||
(:name :visibility :id ebox/visibility :initial visible :contexts (box)
|
||||
:group paint :dirty-kind paint :signature paint)
|
||||
(:name :display :id ebox/display :initial nil :contexts (node)
|
||||
@ -303,6 +306,11 @@ every `ebox-create' needlessly copies the entire property metadata domain.")
|
||||
(when-let* ((property (ebox-style-property name)))
|
||||
(plist-get property :name)))
|
||||
|
||||
(defun ebox-style-property-accepted-p (tag name)
|
||||
"Return non-nil when author form TAG accepts property NAME."
|
||||
(when-let* ((property (ebox-style-property name)))
|
||||
(ebox-style--form-property-allowed-p tag property)))
|
||||
|
||||
(defun ebox-style-schema-id (name)
|
||||
"Return namespaced ECSS schema id for Ebox property NAME."
|
||||
(when-let* ((property (ebox-style-property name)))
|
||||
@ -614,6 +622,20 @@ every `ebox-create' needlessly copies the entire property metadata domain.")
|
||||
"Return non-nil when VALUE is a symbolic font attribute."
|
||||
(or (null value) (symbolp value)))
|
||||
|
||||
(defun ebox-style--wrap-mode-p (value)
|
||||
"Return non-nil when VALUE is a public Box wrapping mode."
|
||||
(memq value '(word char kp none)))
|
||||
|
||||
(defun ebox-style-no-soft-wrap-p (value)
|
||||
"Return non-nil when wrap VALUE disables soft line breaking.
|
||||
`none' is the canonical author value; nil is retained only for private
|
||||
generated wrappers that already contain exact line breaks."
|
||||
(memq value '(nil none)))
|
||||
|
||||
(defun ebox-style-soft-wrap-p (value)
|
||||
"Return non-nil when wrap VALUE performs soft line breaking."
|
||||
(memq value '(word char kp)))
|
||||
|
||||
(defun ebox-style--validator (name)
|
||||
"Return schema validator named NAME."
|
||||
(pcase name
|
||||
@ -629,6 +651,7 @@ every `ebox-create' needlessly copies the entire property metadata domain.")
|
||||
('font-family #'ebox-style--font-family-p)
|
||||
('font-height #'ebox-style--font-height-p)
|
||||
('font-attribute #'ebox-style--font-attribute-p)
|
||||
('wrap-mode #'ebox-style--wrap-mode-p)
|
||||
('boolean (lambda (value) (memq value '(nil t))))
|
||||
(_ (lambda (_value) t))))
|
||||
|
||||
@ -673,6 +696,10 @@ every `ebox-create' needlessly copies the entire property metadata domain.")
|
||||
(defvar ebox-style--closed-computed-cache (make-hash-table :test #'equal)
|
||||
"Bounded cache of selector-free, dependency-free computed styles.")
|
||||
|
||||
(defvar ebox-style--closed-inheritance-cache
|
||||
(make-hash-table :test #'eq :weakness 'key)
|
||||
"Computed-style keyed canonical inheritance projections.")
|
||||
|
||||
(defconst ebox-style--closed-computed-cache-limit 512
|
||||
"Maximum selector-free computed styles retained across projections.")
|
||||
|
||||
@ -690,6 +717,7 @@ every `ebox-create' needlessly copies the entire property metadata domain.")
|
||||
ebox-style-schemas (plist-get property :id)
|
||||
(ebox-style--schema-options property)))
|
||||
(clrhash ebox-style--closed-computed-cache)
|
||||
(clrhash ebox-style--closed-inheritance-cache)
|
||||
(setq ebox-style--registered-schemas ebox-style-schemas))
|
||||
|
||||
(defun ebox-style--ensure-properties ()
|
||||
@ -729,6 +757,53 @@ cascade pass during its initial static projection."
|
||||
(and (symbolp property)
|
||||
(string-prefix-p "--" (symbol-name property))))
|
||||
|
||||
(defun ebox-style--declaration-value-equal-p (left right)
|
||||
"Return non-nil when declaration values LEFT and RIGHT are equivalent.
|
||||
TP paint slots are stable paint addresses. Their installation flag and
|
||||
current face specification belong to TP publication state, not to the Ebox
|
||||
author declaration that references the slot."
|
||||
(if (and (tp-paint-slot-p left) (tp-paint-slot-p right))
|
||||
(eq (tp-paint-slot-face left) (tp-paint-slot-face right))
|
||||
(equal-including-properties left right)))
|
||||
|
||||
(defun ebox-style-declarations-equal-p (left right)
|
||||
"Return non-nil when canonical declaration plists LEFT and RIGHT are equal."
|
||||
(let ((left left)
|
||||
(right right)
|
||||
equal-p)
|
||||
(setq equal-p t)
|
||||
(while (and equal-p left right)
|
||||
(setq equal-p
|
||||
(and (eq (car left) (car right))
|
||||
(ebox-style--declaration-value-equal-p
|
||||
(cadr left) (cadr right)))
|
||||
left (cddr left)
|
||||
right (cddr right)))
|
||||
(and equal-p (null left) (null right))))
|
||||
|
||||
(defun ebox-style--inherited-declarations-changed-p (old new)
|
||||
"Return non-nil when OLD and NEW change descendant style inputs.
|
||||
The inherited property domain comes only from the canonical schema. ECSS
|
||||
custom properties also participate because a descendant `var' expression may
|
||||
consume them even though Ebox does not interpret their names."
|
||||
(or
|
||||
(cl-some
|
||||
(lambda (property)
|
||||
(let ((old-entry (plist-member old property))
|
||||
(new-entry (plist-member new property)))
|
||||
(not (and (eq (not (null old-entry)) (not (null new-entry)))
|
||||
(ebox-style--declaration-value-equal-p
|
||||
(cadr old-entry) (cadr new-entry))))))
|
||||
ebox-style--inherited-properties)
|
||||
(not
|
||||
(ebox-style-declarations-equal-p
|
||||
(cl-loop for (property value) on old by #'cddr
|
||||
when (ebox-style--custom-property-p property)
|
||||
append (list property value))
|
||||
(cl-loop for (property value) on new by #'cddr
|
||||
when (ebox-style--custom-property-p property)
|
||||
append (list property value))))))
|
||||
|
||||
(defun ebox-style--valid-plist-p (plist)
|
||||
"Return non-nil when PLIST is a proper even-length list."
|
||||
(and (proper-list-p plist) (zerop (% (length plist) 2))))
|
||||
@ -816,7 +891,9 @@ not affect declaration compilation and are intentionally excluded."
|
||||
(and (numberp value) (not (ebox-style--positive-integer-p value))))
|
||||
('grid-placement
|
||||
(and (or (numberp value) (proper-list-p value))
|
||||
(not (ebox-style--grid-placement-p value))))))
|
||||
(not (ebox-style--grid-placement-p value))))
|
||||
('wrap-mode
|
||||
(not (ebox-style--wrap-mode-p value)))))
|
||||
|
||||
(defun ebox-style--validate-declaration-values (declarations)
|
||||
"Return DECLARATIONS after static Ebox-owned value validation."
|
||||
@ -860,6 +937,65 @@ When STRICT is non-nil, reject properties outside the Ebox style domain."
|
||||
(puthash cache-key compiled ebox-style--declaration-cache)
|
||||
(copy-sequence compiled))))))
|
||||
|
||||
(defconst ebox-style--text-form-properties
|
||||
'(:color :background-color
|
||||
:font :font-family :font-height :font-weight :font-slant)
|
||||
"Canonical property names accepted by the Text author form.")
|
||||
|
||||
(defun ebox-style--form-property-allowed-p (tag property)
|
||||
"Return non-nil when author TAG accepts PROPERTY metadata."
|
||||
(let ((name (plist-get property :name))
|
||||
(contexts (plist-get property :contexts)))
|
||||
(if (eq tag 'text)
|
||||
(memq name ebox-style--text-form-properties)
|
||||
(or (memq 'box contexts)
|
||||
(memq 'item contexts)
|
||||
(and (memq tag '(flex grid))
|
||||
(memq tag contexts))))))
|
||||
|
||||
(defun ebox-style-validate-form-declarations (tag declarations)
|
||||
"Validate canonical DECLARATIONS for Text/Box author TAG."
|
||||
(unless (memq tag '(text box row column flex grid))
|
||||
(error "Unknown Ebox author form: %S" tag))
|
||||
(unless (ebox-style--valid-plist-p declarations)
|
||||
(error "Ebox canonical declarations must be an even plist: %S"
|
||||
declarations))
|
||||
(let ((seen (make-hash-table :test #'eq)))
|
||||
(cl-loop for (id _value) on declarations by #'cddr
|
||||
for property = (ebox-style-property id)
|
||||
unless (and property (eq id (plist-get property :id)))
|
||||
do (error "Ebox declaration is not a canonical property ID: %S"
|
||||
id)
|
||||
do (when (gethash id seen)
|
||||
(error "Duplicate canonical Ebox declaration: %S" id))
|
||||
do (puthash id t seen)
|
||||
unless (ebox-style--form-property-allowed-p tag property)
|
||||
do (error "Ebox %S does not accept %S"
|
||||
tag (plist-get property :name))))
|
||||
(ebox-style--validate-declaration-values declarations)
|
||||
declarations)
|
||||
|
||||
(defun ebox-style-compile-form (tag plist)
|
||||
"Compile evaluated author PLIST once and validate it for TAG."
|
||||
(ebox-style-validate-form-declarations
|
||||
tag (ebox-style-compile-declarations plist t)))
|
||||
|
||||
(defun ebox-style-declaration-properties (declarations predicate)
|
||||
"Return canonical author properties in DECLARATIONS matching PREDICATE."
|
||||
(cl-loop for (id value) on declarations by #'cddr
|
||||
for property = (or (ebox-style-property id)
|
||||
(error "Unknown canonical Ebox property: %S" id))
|
||||
when (funcall predicate property)
|
||||
append (list (plist-get property :name) value)))
|
||||
|
||||
(defun ebox-style-declarations-require-projection-p
|
||||
(declarations predicate)
|
||||
"Return non-nil when DECLARATIONS include facts outside PREDICATE."
|
||||
(cl-loop for (id _value) on declarations by #'cddr
|
||||
for property = (or (ebox-style-property id)
|
||||
(error "Unknown canonical Ebox property: %S" id))
|
||||
thereis (not (funcall predicate property))))
|
||||
|
||||
(defun ebox-style-merge-declarations (base overrides)
|
||||
"Merge canonical BASE and OVERRIDES through ECSS property schemas."
|
||||
(ebox-style--ensure-properties)
|
||||
@ -949,9 +1085,10 @@ winner is required so parent inherited geometry/paint cannot be stale."
|
||||
(plist-member new 'ebox/color)))
|
||||
|
||||
(defconst ebox-style--inherited-properties
|
||||
'(ebox/color ebox/font ebox/font-family ebox/font-height
|
||||
ebox/font-weight ebox/font-slant)
|
||||
"Inherited ECSS properties whose parent values guard Theme deltas.")
|
||||
(cl-loop for property in ebox-style--property-definitions
|
||||
when (plist-get property :inherits)
|
||||
collect (plist-get property :id))
|
||||
"Canonical ECSS properties inherited according to the shared schema.")
|
||||
|
||||
(defun ebox-style--inherited-style-signature (style)
|
||||
"Return the inherited-value fingerprint of computed STYLE, or nil.
|
||||
@ -1165,13 +1302,25 @@ SNAPSHOT reuses a previously detached ECSS values snapshot when supplied."
|
||||
(ecss-computed-style-specified-p style property))
|
||||
|
||||
(defun ebox-style-node-declarations (node)
|
||||
"Return canonical ECSS declarations stored on semantic Ebox NODE."
|
||||
"Return NODE's immutable canonical ECSS declarations.
|
||||
Callers that need to mutate the declaration spine must copy it first."
|
||||
(let ((declarations (plist-get node :ebox-style-declarations)))
|
||||
(if (memq (plist-get node :ebox-type) '(flex grid))
|
||||
(ebox-style-merge-declarations
|
||||
declarations
|
||||
(plist-get (plist-get node :box) :ebox-style-overrides))
|
||||
(copy-sequence declarations))))
|
||||
declarations)))
|
||||
|
||||
(defun ebox-style-node-specified-value (node name &optional missing)
|
||||
"Return NODE's canonical specified value for author property NAME.
|
||||
Return MISSING when NAME is valid but not specified. Aliases resolve to the
|
||||
same canonical property id, so callers never inspect backend plist fields."
|
||||
(let ((id (or (ebox-style-schema-id name)
|
||||
(error "Unknown Ebox property: %S" name)))
|
||||
(declarations (ebox-style-node-declarations node)))
|
||||
(if (plist-member declarations id)
|
||||
(plist-get declarations id)
|
||||
missing)))
|
||||
|
||||
(defun ebox-style--remove-style-properties (plist)
|
||||
"Return PLIST without Ebox style or ECSS custom properties."
|
||||
@ -1195,6 +1344,38 @@ SNAPSHOT reuses a previously detached ECSS values snapshot when supplied."
|
||||
do (plist-put box property value)))
|
||||
box)
|
||||
|
||||
(defvar ebox-style--box-engine-longhand-cache
|
||||
(make-hash-table :test #'eq :weakness 'key)
|
||||
"Computed-style keyed expanded engine longhands for canonical Box nodes.")
|
||||
|
||||
(defvar ebox-style--text-engine-longhand-cache
|
||||
(make-hash-table :test #'eq :weakness 'key)
|
||||
"Computed-style keyed expanded engine longhands for canonical Text nodes.")
|
||||
|
||||
(defun ebox-style--cached-engine-longhands (style text-p snapshot)
|
||||
"Return STYLE's expanded engine longhands for TEXT-P and SNAPSHOT."
|
||||
(let* ((cache (if text-p
|
||||
ebox-style--text-engine-longhand-cache
|
||||
ebox-style--box-engine-longhand-cache))
|
||||
(missing (make-symbol "ebox-engine-longhands-missing"))
|
||||
(cached (gethash style cache missing)))
|
||||
(if (not (eq cached missing))
|
||||
cached
|
||||
(let ((expanded
|
||||
(ebox-style-expand-ebox-plist
|
||||
(if text-p
|
||||
(ebox-style--text-values style snapshot)
|
||||
(ebox-style--box-values style snapshot)))))
|
||||
(puthash style expanded cache)
|
||||
expanded))))
|
||||
|
||||
(defun ebox-style--apply-engine-longhands (box longhands)
|
||||
"Project already expanded LONGHANDS into BOX engine fields."
|
||||
(ebox-style--reset-box-engine-style box)
|
||||
(cl-loop for (property value) on longhands by #'cddr
|
||||
do (plist-put box property value))
|
||||
box)
|
||||
|
||||
(defun ebox-style--box-values (style &optional snapshot)
|
||||
"Return computed box values ready for Ebox engine projection."
|
||||
(let* ((snapshot (or snapshot (ebox-style--computed-snapshot style)))
|
||||
@ -1205,6 +1386,47 @@ SNAPSHOT reuses a previously detached ECSS values snapshot when supplied."
|
||||
(plist-put values :color ebox-style--default-foreground)))
|
||||
values))
|
||||
|
||||
(defun ebox-style--text-values (style snapshot)
|
||||
"Return computed STYLE values owned by canonical Text."
|
||||
(cl-loop for (name value) on (ebox-style--box-values style snapshot)
|
||||
by #'cddr
|
||||
when (or (ebox-style-property-accepted-p 'text name)
|
||||
(eq name :wrap-mode))
|
||||
append (list name value)))
|
||||
|
||||
(defun ebox-style--closed-inheritance-values (style)
|
||||
"Return STYLE's immutable canonical inheritance projection."
|
||||
(let* ((missing (make-symbol "ebox-inheritance-missing"))
|
||||
(cached (gethash style ebox-style--closed-inheritance-cache missing)))
|
||||
(if (not (eq cached missing))
|
||||
cached
|
||||
(let* ((snapshot (ebox-style--computed-snapshot style))
|
||||
(values (ebox-style--box-values style snapshot))
|
||||
result)
|
||||
(dolist (property '(:color :font :font-family :font-height
|
||||
:font-weight :font-slant :wrap-mode))
|
||||
(when (plist-member values property)
|
||||
(setq result
|
||||
(plist-put result property (plist-get values property)))))
|
||||
(puthash style result ebox-style--closed-inheritance-cache)
|
||||
result))))
|
||||
|
||||
(defun ebox-style-apply-closed-inheritance (node style)
|
||||
"Apply only closed computed inheritance from STYLE to fresh canonical NODE."
|
||||
(let ((values (ebox-style--closed-inheritance-values style)))
|
||||
(plist-put node :ebox-computed-style style)
|
||||
(cl-loop for (property value) on values by #'cddr
|
||||
do (plist-put node property value)))
|
||||
node)
|
||||
|
||||
(defun ebox-style--text-paint-owner-p (node)
|
||||
"Return non-nil when NODE materializes inherited text paint itself.
|
||||
Canonical Text owns typography and foreground paint. Canonical Box retains
|
||||
those computed facts for inheritance but materializes only box paint such as
|
||||
background and borders. Legacy runtime boxes keep their established behavior
|
||||
until their private encoding is removed."
|
||||
(not (eq (plist-get node :ebox-kind) 'box)))
|
||||
|
||||
(defconst ebox-style--container-wrapper-neutral-properties
|
||||
'(:width :height :box-sizing :color
|
||||
:font :font-family :font-height :font-weight :font-slant)
|
||||
@ -1221,8 +1443,9 @@ SNAPSHOT reuses a previously detached ECSS values snapshot when supplied."
|
||||
|
||||
(defun ebox-style--new-container-wrapper ()
|
||||
"Return a stable internal box for container-owned box paint."
|
||||
(let ((wrapper (ebox-create :content "" :wrap-mode nil
|
||||
:key 'ebox/style-wrapper)))
|
||||
(let ((wrapper
|
||||
(ebox-node-factory--create-from-properties
|
||||
:content "" :wrap-mode nil :key 'ebox/style-wrapper)))
|
||||
(plist-put wrapper :ebox-style-wrapper t)
|
||||
(plist-put wrapper :ebox-style-generated-wrapper t)
|
||||
wrapper))
|
||||
@ -1349,32 +1572,16 @@ SNAPSHOT reuses a previously detached ECSS values snapshot when supplied."
|
||||
(ebox-style--apply-display-axes node style snapshot)
|
||||
(pcase type
|
||||
('box
|
||||
(ebox-style--apply-engine-values
|
||||
(ebox-style--apply-engine-longhands
|
||||
node
|
||||
(ebox-style--box-values style snapshot)))
|
||||
(ebox-style--cached-engine-longhands
|
||||
style (eq (plist-get node :ebox-kind) 'text) snapshot))
|
||||
)
|
||||
((or 'flex 'grid)
|
||||
(ebox-style--apply-container node style snapshot)))
|
||||
(ebox-style--apply-item node style snapshot))
|
||||
node)
|
||||
|
||||
(defun ebox-style-sync-flex-item (item)
|
||||
"Synchronize internal Flex ITEM props from its semantic source style."
|
||||
(when (eq (plist-get item :ebox-type) 'flex-item)
|
||||
(when-let* ((source (plist-get item :node)))
|
||||
(let* ((source-style (plist-get source :ebox-computed-style))
|
||||
(child-style
|
||||
(when-let* ((child (plist-get source :ebox-content-node)))
|
||||
(plist-get child :ebox-computed-style)))
|
||||
(values
|
||||
(append
|
||||
(and source-style
|
||||
(ebox-style--context-values source-style 'item))
|
||||
(and child-style
|
||||
(ebox-style--context-values child-style 'item)))))
|
||||
(when (or source-style child-style)
|
||||
(plist-put item :props values)))))
|
||||
item)
|
||||
|
||||
(defun ebox-style-dirty-kind (name)
|
||||
"Return dirty kind for canonical property or alias NAME."
|
||||
(when-let* ((property (ebox-style-property name)))
|
||||
|
||||
1035
ebox-surface.el
1035
ebox-surface.el
File diff suppressed because it is too large
Load Diff
569
ebox-tree.el
569
ebox-tree.el
@ -29,11 +29,6 @@
|
||||
"Dynamic snapshot-detail-local node -> region ids cache.
|
||||
This is an optimization for snapshot capture, not buffer runtime state.")
|
||||
(defvar ebox--flex-item-prop-keys)
|
||||
(defvar ebox--flex-item-direct-adapter-token)
|
||||
(defvar ebox--flex-item-wrapper-adapter-token)
|
||||
(defvar ebox-tree--legacy-flex-item-adapter-proofs
|
||||
(make-hash-table :test #'eq :weakness 'key)
|
||||
"Weak adapter -> construction-shape proofs for legacy Flex items.")
|
||||
|
||||
(defun ebox-tree-computed-display (node)
|
||||
"Return NODE's canonical CSS-like display pair."
|
||||
@ -44,7 +39,6 @@ This is an optimization for snapshot capture, not buffer runtime state.")
|
||||
('stack '(block column))
|
||||
('flex '(block flex))
|
||||
('grid '(block grid))
|
||||
('flex-item '(block flow))
|
||||
(_ ebox--default-display))))
|
||||
|
||||
(defun ebox-tree-display-outer (node)
|
||||
@ -59,16 +53,29 @@ This is an optimization for snapshot capture, not buffer runtime state.")
|
||||
"Return non-nil when NODE establishes a child layout context."
|
||||
(memq (ebox-tree-display-inner node) '(row column flex grid)))
|
||||
|
||||
(defun ebox-tree-layout-props (node)
|
||||
"Return NODE's canonical child-layout properties.
|
||||
Typed Box nodes read their LayoutConfig; legacy runtime adapters fall back to
|
||||
their private props encoding. Callers receive a detached value."
|
||||
(if-let* ((config (and (listp node)
|
||||
(plist-get node :ebox-layout-config))))
|
||||
(copy-tree (ebox-layout-config-props config))
|
||||
(copy-tree (or (and (listp node) (plist-get node :raw-props))
|
||||
(and (listp node) (plist-get node :props))))))
|
||||
|
||||
(defun ebox-tree-node-selector-type (node)
|
||||
"Return NODE's CSS-like selector type symbol."
|
||||
(pcase (and (listp node) (plist-get node :ebox-type))
|
||||
('box 'box)
|
||||
('concat 'row)
|
||||
('stack 'column)
|
||||
('flex 'flex)
|
||||
('grid 'grid)
|
||||
('flex-item 'item)
|
||||
(_ nil)))
|
||||
(or (pcase (and (listp node) (plist-get node :ebox-kind))
|
||||
('text 'text)
|
||||
('box 'box)
|
||||
(_ nil))
|
||||
(pcase (and (listp node) (plist-get node :ebox-type))
|
||||
('box 'box)
|
||||
('concat 'row)
|
||||
('stack 'column)
|
||||
('flex 'flex)
|
||||
('grid 'grid)
|
||||
(_ nil))))
|
||||
|
||||
(defun ebox-tree-metadata-string (value)
|
||||
"Return VALUE normalized to a selector metadata string."
|
||||
@ -143,7 +150,20 @@ This is an optimization for snapshot capture, not buffer runtime state.")
|
||||
|
||||
(defun ebox-tree--children-raw (node)
|
||||
"Return NODE's direct child nodes without assigning runtime ids."
|
||||
(pcase (and (listp node) (plist-get node :ebox-type))
|
||||
(let ((type (and (listp node) (plist-get node :ebox-type))))
|
||||
(cond
|
||||
;; Flex and Grid own a visual wrapper as well as their layout children.
|
||||
;; Runtime traversals must see both through this single child contract;
|
||||
;; otherwise rendered wrapper regions have no node or surface identity.
|
||||
((memq type '(flex grid))
|
||||
(append (when-let* ((box (plist-get node :box))) (list box))
|
||||
(ebox-tree-layout-children node)))
|
||||
((and (listp node) (plist-member node :children))
|
||||
(plist-get node :children))
|
||||
((and (listp node) (plist-get node :ebox-child-sequence))
|
||||
(ebox-child-range--flatten (plist-get node :ebox-child-sequence)))
|
||||
(t
|
||||
(pcase type
|
||||
('box
|
||||
(if (plist-member node :children)
|
||||
(plist-get node :children)
|
||||
@ -152,17 +172,7 @@ This is an optimization for snapshot capture, not buffer runtime state.")
|
||||
(ebox-tree-layout-children node))
|
||||
('stack
|
||||
(ebox-tree-layout-children node))
|
||||
('flex
|
||||
(append (when-let* ((box (plist-get node :box)))
|
||||
(list box))
|
||||
(ebox-tree-layout-children node)))
|
||||
('grid
|
||||
(append (when-let* ((box (plist-get node :box)))
|
||||
(list box))
|
||||
(ebox-tree-layout-children node)))
|
||||
('flex-item
|
||||
(delq nil (list (plist-get node :node))))
|
||||
(_ nil)))
|
||||
(_ nil))))))
|
||||
|
||||
(defun ebox-tree-node-children (node)
|
||||
"Return NODE's direct declarative children without mutating them.
|
||||
@ -189,7 +199,10 @@ must not modify a published runtime tree through this accessor."
|
||||
('box
|
||||
(if (plist-member node :children)
|
||||
(dolist (child (plist-get node :children))
|
||||
(funcall function child))
|
||||
(if (ebox-child-range--descriptor-p child)
|
||||
(dolist (item (ebox-child-range--descriptor-items child))
|
||||
(funcall function item))
|
||||
(funcall function child)))
|
||||
(when-let* ((child (plist-get node :ebox-content-node)))
|
||||
(funcall function child))))
|
||||
((or 'concat 'stack 'flex 'grid)
|
||||
@ -202,19 +215,76 @@ must not modify a published runtime tree through this accessor."
|
||||
('stack
|
||||
(delq nil (list (plist-get node :top)
|
||||
(plist-get node :bottom)))))))
|
||||
(funcall function child)))
|
||||
('flex-item
|
||||
(when-let* ((child (plist-get node :node)))
|
||||
(funcall function child))))))
|
||||
(if (ebox-child-range--descriptor-p child)
|
||||
(dolist (item (ebox-child-range--descriptor-items child))
|
||||
(funcall function item))
|
||||
(funcall function child))))
|
||||
(_ nil))))
|
||||
node)
|
||||
|
||||
(defun ebox-tree-map-direct-children (node function)
|
||||
"Return FUNCTION results for NODE's children without an intermediate list."
|
||||
(let (results)
|
||||
(ebox-tree-for-each-direct-child
|
||||
node (lambda (child) (push (funcall function child) results)))
|
||||
(nreverse results)))
|
||||
|
||||
(defun ebox-tree-author-style-count (node)
|
||||
"Return NODE's retained count of style-projecting canonical descendants."
|
||||
(let ((count (and (listp node)
|
||||
(plist-get node :ebox-author-style-count))))
|
||||
(if (and (integerp count) (>= count 0))
|
||||
count
|
||||
(if (and (listp node)
|
||||
(plist-get node :ebox-author-style-required-p))
|
||||
1
|
||||
0))))
|
||||
|
||||
(defun ebox-tree--set-author-style-count (node count)
|
||||
"Set NODE's exact nonnegative author style COUNT and return NODE."
|
||||
(unless (and (integerp count) (>= count 0))
|
||||
(error "Ebox author style count must be nonnegative: %S" count))
|
||||
(plist-put node :ebox-author-style-count count)
|
||||
(if (> count 0)
|
||||
(plist-put node :ebox-author-style-required-p t)
|
||||
(cl-remf node :ebox-author-style-required-p))
|
||||
node)
|
||||
|
||||
(defun ebox-tree-clear-author-style-pending (node)
|
||||
"Clear projected author style facts in NODE's pending subtree."
|
||||
(when (> (ebox-tree-author-style-count node) 0)
|
||||
(ebox-tree-for-each-direct-child
|
||||
node
|
||||
(lambda (child)
|
||||
(when (> (ebox-tree-author-style-count child) 0)
|
||||
(ebox-tree-clear-author-style-pending child))))
|
||||
(ebox-tree--set-author-style-count node 0))
|
||||
node)
|
||||
|
||||
(cl-defun ebox-tree--put-child-sequence
|
||||
(node sequence &optional (children nil children-p))
|
||||
"Set NODE's authoritative child SEQUENCE and retained material CHILDREN.
|
||||
When CHILDREN is omitted, invalidate the retained material list without
|
||||
flattening; the next layout consumer may materialize it exactly once."
|
||||
(plist-put node :ebox-child-sequence sequence)
|
||||
(if children-p
|
||||
(plist-put node :children children)
|
||||
(cl-remf node :children))
|
||||
node)
|
||||
|
||||
(defun ebox-tree-layout-children (node)
|
||||
"Return layout container children for NODE in render order.
|
||||
New row/column containers store flat `:children'. Legacy `ebox-concat' and
|
||||
`ebox-stack' nodes may still carry `:left'/`:right' or `:top'/`:bottom'."
|
||||
(if (plist-member node :ebox-child-sequence)
|
||||
(ebox-child-range--flatten (plist-get node :ebox-child-sequence))
|
||||
(or (plist-get node :children)
|
||||
(if (plist-member node :children)
|
||||
(plist-get node :children)
|
||||
(if (plist-member node :ebox-child-sequence)
|
||||
(let ((children
|
||||
(ebox-child-range--flatten
|
||||
(plist-get node :ebox-child-sequence))))
|
||||
(plist-put node :children children)
|
||||
children)
|
||||
(or (plist-get node :children)
|
||||
(pcase (and (listp node) (plist-get node :ebox-type))
|
||||
('concat
|
||||
(delq nil (list (plist-get node :left)
|
||||
@ -222,7 +292,7 @@ New row/column containers store flat `:children'. Legacy `ebox-concat' and
|
||||
('stack
|
||||
(delq nil (list (plist-get node :top)
|
||||
(plist-get node :bottom))))
|
||||
(_ nil)))))
|
||||
(_ nil))))))
|
||||
|
||||
(defun ebox-tree--replace-direct-child (child replacements)
|
||||
"Return CHILD's identity replacement from REPLACEMENTS, when present.
|
||||
@ -232,24 +302,27 @@ child and is therefore never used as a replacement key."
|
||||
(cdr replacement)
|
||||
child))
|
||||
|
||||
(defun ebox-tree--replace-direct-child-list (children replacements)
|
||||
"Replace CHILDREN found by identity in REPLACEMENTS.
|
||||
Return CHILDREN itself when no child changes. Otherwise return one new list
|
||||
whose untouched child objects remain shared. A nil replacement removes the
|
||||
matched child from the returned sequence."
|
||||
(defun ebox-tree--replace-direct-child-list-result (children replacements)
|
||||
"Return `(CHILDREN . STYLE-DELTA)' after identity REPLACEMENTS."
|
||||
(if (null replacements)
|
||||
children
|
||||
(let (changed replaced)
|
||||
(cons children 0)
|
||||
(let ((style-delta 0) changed replaced)
|
||||
(dolist (child children)
|
||||
(let ((replacement
|
||||
(ebox-tree--replace-direct-child child replacements)))
|
||||
(unless (eq replacement child)
|
||||
(setq changed t))
|
||||
(setq changed t)
|
||||
(cl-incf style-delta
|
||||
(- (ebox-tree-author-style-count replacement)
|
||||
(ebox-tree-author-style-count child))))
|
||||
(when replacement
|
||||
(push replacement replaced))))
|
||||
(if changed
|
||||
(nreverse replaced)
|
||||
children))))
|
||||
(cons (if changed (nreverse replaced) children)
|
||||
style-delta))))
|
||||
|
||||
(defun ebox-tree--replace-direct-child-list (children replacements)
|
||||
"Replace CHILDREN found by identity in REPLACEMENTS."
|
||||
(car (ebox-tree--replace-direct-child-list-result children replacements)))
|
||||
|
||||
(defun ebox-tree--replace-direct-child-property
|
||||
(node property replacements)
|
||||
@ -280,14 +353,15 @@ still copied while every nested value remains shared."
|
||||
(error "Ebox can only path-copy a node plist, got %S" node))
|
||||
(let* ((type (plist-get node :ebox-type))
|
||||
(copy (copy-sequence node))
|
||||
(sequence (plist-get copy :ebox-child-sequence))
|
||||
(style-delta 0)
|
||||
(scalar-properties
|
||||
(pcase type
|
||||
('box '(:ebox-content-node))
|
||||
('concat '(:left :right))
|
||||
('stack '(:top :bottom))
|
||||
('flex '(:box))
|
||||
('grid '(:box))
|
||||
('flex-item '(:node)))))
|
||||
('grid '(:box)))))
|
||||
(dolist (property scalar-properties)
|
||||
(setq copy
|
||||
(ebox-tree--replace-direct-child-property
|
||||
@ -296,13 +370,16 @@ still copied while every nested value remains shared."
|
||||
(and (eq type 'box) (plist-member copy :children)))
|
||||
(plist-member copy :children))
|
||||
(let* ((children (plist-get copy :children))
|
||||
(replaced
|
||||
(ebox-tree--replace-direct-child-list children replacements)))
|
||||
(result
|
||||
(ebox-tree--replace-direct-child-list-result
|
||||
children replacements))
|
||||
(replaced (car result)))
|
||||
(unless (eq replaced children)
|
||||
(setq copy (plist-put copy :children replaced)))))
|
||||
(when-let* ((sequence (and (memq type '(concat stack flex grid))
|
||||
(plist-get copy :ebox-child-sequence))))
|
||||
(let ((updated sequence) changed fallback)
|
||||
(setq copy (plist-put copy :children replaced)))
|
||||
(unless sequence
|
||||
(setq style-delta (cdr result)))))
|
||||
(when sequence
|
||||
(let ((updated sequence) changed fallback identity-locations)
|
||||
(dolist (replacement replacements)
|
||||
(let* ((old (car replacement)) (new (cdr replacement))
|
||||
(location (and old (plist-get old :ebox-sequence-location)))
|
||||
@ -325,9 +402,28 @@ still copied while every nested value remains shared."
|
||||
(list :parent-node-id (plist-get node :node-id)
|
||||
:segment-index (car key-location)
|
||||
:offset (cdr key-location))))))
|
||||
(unless location
|
||||
(unless identity-locations
|
||||
(setq identity-locations (make-hash-table :test #'eq))
|
||||
(dotimes (segment-index
|
||||
(ebox-child-range--sequence-count sequence))
|
||||
(let ((payload
|
||||
(ebox-child-range--segment-payload
|
||||
(ebox-child-range--segment-at
|
||||
sequence segment-index))))
|
||||
(dotimes (offset (length payload))
|
||||
(puthash
|
||||
(aref payload offset)
|
||||
(list :parent-node-id (plist-get node :node-id)
|
||||
:segment-index segment-index :offset offset)
|
||||
identity-locations)))))
|
||||
(setq location (gethash old identity-locations)))
|
||||
(when (and location
|
||||
(equal (plist-get location :parent-node-id)
|
||||
(plist-get node :node-id)))
|
||||
(cl-incf style-delta
|
||||
(- (ebox-tree-author-style-count new)
|
||||
(ebox-tree-author-style-count old)))
|
||||
(if new
|
||||
(setq updated
|
||||
(ebox-child-range--replace-item-at
|
||||
@ -345,10 +441,19 @@ still copied while every nested value remains shared."
|
||||
(ebox-tree--replace-direct-child-list
|
||||
items replacements))
|
||||
segments)))
|
||||
(setq updated (ebox-child-range--build (nreverse segments))
|
||||
(setq updated (ebox-child-range--build
|
||||
(nreverse segments) nil t)
|
||||
changed t)))
|
||||
(when changed
|
||||
(setq copy (plist-put copy :ebox-child-sequence updated)))))
|
||||
(setq copy
|
||||
(if (plist-member copy :children)
|
||||
(ebox-tree--put-child-sequence
|
||||
copy updated (plist-get copy :children))
|
||||
(ebox-tree--put-child-sequence copy updated))))))
|
||||
(when (eq (plist-get node :ebox-kind) 'box)
|
||||
(setq copy
|
||||
(ebox-tree--set-author-style-count
|
||||
copy (+ (ebox-tree-author-style-count node) style-delta))))
|
||||
copy))
|
||||
|
||||
(defun ebox-tree-copy-node-structure (root)
|
||||
@ -365,7 +470,8 @@ original object identity."
|
||||
(if (or (not (listp node)) (stringp node))
|
||||
node
|
||||
(let* ((type (plist-get node :ebox-type))
|
||||
(material-p (memq type '(concat stack flex grid)))
|
||||
(material-p (or (memq type '(concat stack flex grid))
|
||||
(plist-get node :ebox-child-sequence)))
|
||||
(raw (and material-p (plist-get node :children)))
|
||||
(sequence (and material-p
|
||||
(plist-get node :ebox-child-sequence)))
|
||||
@ -393,7 +499,7 @@ original object identity."
|
||||
(ebox-child-range--descriptor-items entry)))
|
||||
segments)
|
||||
(push (cons nil (list (copy-node entry))) segments))))
|
||||
(setq copy (without copy :children))
|
||||
(setq segments (nreverse segments))
|
||||
(pcase type
|
||||
((or 'flex 'grid)
|
||||
(when-let* ((box (plist-get node :box)))
|
||||
@ -402,19 +508,19 @@ original object identity."
|
||||
(setq copy (without (without copy :left) :right)))
|
||||
('stack
|
||||
(setq copy (without (without copy :top) :bottom))))
|
||||
(plist-put copy :ebox-child-sequence
|
||||
(ebox-child-range--build (nreverse segments))))
|
||||
(ebox-tree--put-child-sequence
|
||||
copy
|
||||
(ebox-child-range--build segments nil t)
|
||||
(cl-mapcan (lambda (segment)
|
||||
(copy-sequence (cdr segment)))
|
||||
segments)))
|
||||
(let ((copy
|
||||
(ebox-tree-copy-with-direct-child-replacements
|
||||
node
|
||||
(mapcar (lambda (child)
|
||||
(cons child (copy-node child)))
|
||||
(ebox-tree--children-raw node)))))
|
||||
;; Only this exact deep-copy traversal has proven every copied
|
||||
;; child derives from the authentic source tree. Generic
|
||||
;; path-copy replacements must not launder adapter provenance.
|
||||
(ebox-tree--remint-legacy-flex-item-adapter-proof
|
||||
node copy)))))))
|
||||
copy))))))
|
||||
(copy-node root)))
|
||||
|
||||
(defun ebox-tree--runtime-identity-node-shell
|
||||
@ -447,13 +553,14 @@ Record its identities in NODE-ID-SET and REGION-ID-SET."
|
||||
node node-id-set region-id-set)))
|
||||
(pcase (plist-get node :ebox-type)
|
||||
('box
|
||||
(if (plist-member node :children)
|
||||
(if (or (plist-member node :children)
|
||||
(plist-get node :ebox-child-sequence))
|
||||
(plist-put shell :children
|
||||
(mapcar
|
||||
(ebox-tree-map-direct-children
|
||||
node
|
||||
(lambda (child)
|
||||
(ebox-tree--runtime-identity-skeleton
|
||||
child node-id-set region-id-set))
|
||||
(plist-get node :children)))
|
||||
child node-id-set region-id-set))))
|
||||
(when-let* ((child (plist-get node :ebox-content-node)))
|
||||
(plist-put shell :ebox-content-node
|
||||
(ebox-tree--runtime-identity-skeleton
|
||||
@ -475,12 +582,7 @@ Record its identities in NODE-ID-SET and REGION-ID-SET."
|
||||
(lambda (child)
|
||||
(ebox-tree--runtime-identity-skeleton
|
||||
child node-id-set region-id-set))
|
||||
(ebox-tree-layout-children node))))
|
||||
('flex-item
|
||||
(when-let* ((child (plist-get node :node)))
|
||||
(plist-put shell :node
|
||||
(ebox-tree--runtime-identity-skeleton
|
||||
child node-id-set region-id-set)))))
|
||||
(ebox-tree-layout-children node)))))
|
||||
shell))
|
||||
|
||||
(defun ebox-tree-runtime-identity-snapshot (root)
|
||||
@ -530,11 +632,7 @@ Host references are root-global opaque metadata and are compared with `equal'."
|
||||
(unless (gethash node seen)
|
||||
(puthash node t seen)
|
||||
(puthash node t active)
|
||||
(dolist (child (ebox-tree--children-raw node))
|
||||
(if (ebox-child-range--descriptor-p child)
|
||||
(dolist (item (ebox-child-range--descriptor-items child))
|
||||
(visit item))
|
||||
(visit child)))
|
||||
(ebox-tree-for-each-direct-child node #'visit)
|
||||
(remhash node active))))))
|
||||
(visit root))
|
||||
root))
|
||||
@ -570,28 +668,13 @@ Host references are root-global opaque metadata and are compared with `equal'."
|
||||
|
||||
(defun ebox-tree--author-specifies-participation-p (node key)
|
||||
"Return non-nil when NODE's author source specifies participation KEY."
|
||||
(if (eq (plist-get node :ebox-type) 'flex-item)
|
||||
(let* ((adapter-source (plist-get node :node))
|
||||
(content (and (listp adapter-source)
|
||||
(plist-get adapter-source :ebox-content-node)))
|
||||
(projected-p
|
||||
(or (and (listp adapter-source)
|
||||
(plist-get adapter-source :ebox-computed-style))
|
||||
(and (listp content)
|
||||
(plist-get content :ebox-computed-style)))))
|
||||
(or (ebox-tree--declarations-specify-participation-p
|
||||
adapter-source key)
|
||||
(ebox-tree--declarations-specify-participation-p content key)
|
||||
(and (not projected-p)
|
||||
(plist-member (plist-get node :props) key))))
|
||||
(or (ebox-tree--declarations-specify-participation-p node key)
|
||||
(and (null (plist-get node :ebox-computed-style))
|
||||
(plist-member node key)))))
|
||||
(or (ebox-tree--declarations-specify-participation-p node key)
|
||||
(and (null (plist-get node :ebox-computed-style))
|
||||
(plist-member node key))))
|
||||
|
||||
(defun ebox-tree--node-participation-keys (node source)
|
||||
"Return NODE participation keys from AUTHOR or COMPUTED SOURCE."
|
||||
(let ((flex-item-p (eq (plist-get node :ebox-type) 'flex-item))
|
||||
(style (plist-get node :ebox-computed-style))
|
||||
(let ((style (plist-get node :ebox-computed-style))
|
||||
keys)
|
||||
(dolist (key ebox-tree--participation-keys)
|
||||
(when
|
||||
@ -599,33 +682,16 @@ Host references are root-global opaque metadata and are compared with `equal'."
|
||||
('author
|
||||
(ebox-tree--author-specifies-participation-p node key))
|
||||
('computed
|
||||
(if flex-item-p
|
||||
(let* ((adapter-source (plist-get node :node))
|
||||
(adapter-style
|
||||
(and (listp adapter-source)
|
||||
(plist-get adapter-source
|
||||
:ebox-computed-style)))
|
||||
(content (and (listp adapter-source)
|
||||
(plist-get adapter-source
|
||||
:ebox-content-node)))
|
||||
(content-style
|
||||
(and (listp content)
|
||||
(plist-get content :ebox-computed-style))))
|
||||
(and (plist-member (plist-get node :props) key)
|
||||
(or (ebox-tree--style-specifies-participation-p
|
||||
adapter-style key)
|
||||
(ebox-tree--style-specifies-participation-p
|
||||
content-style key))))
|
||||
(and (plist-member node key)
|
||||
(ebox-tree--style-specifies-participation-p style key))))
|
||||
(and (plist-member node key)
|
||||
(ebox-tree--style-specifies-participation-p style key)))
|
||||
(_ (error "Unknown Ebox participation source: %S" source)))
|
||||
(push key keys)))
|
||||
(nreverse keys)))
|
||||
|
||||
(defun ebox-tree--child-layout-kind (node parent-kind)
|
||||
"Return layout kind governing NODE children, preserving transparent adapters."
|
||||
"Return the layout kind governing NODE's direct children."
|
||||
(ignore parent-kind)
|
||||
(pcase (plist-get node :ebox-type)
|
||||
('flex-item parent-kind)
|
||||
('flex 'flex)
|
||||
('grid 'grid)
|
||||
(_ (pcase (if (and (eq (plist-get node :ebox-kind) 'box)
|
||||
@ -636,112 +702,34 @@ Host references are root-global opaque metadata and are compared with `equal'."
|
||||
('flow 'normal)
|
||||
(kind kind)))))
|
||||
|
||||
(defun ebox-tree--legacy-flex-item-adapter-p (node token)
|
||||
"Return non-nil when NODE has the exact legacy Flex adapter TOKEN shape."
|
||||
(let ((proof (gethash node ebox-tree--legacy-flex-item-adapter-proofs))
|
||||
(source (plist-get node :node)))
|
||||
(and token
|
||||
(eq (plist-get node :ebox-type) 'flex-item)
|
||||
(vectorp proof)
|
||||
(= (length proof) 3)
|
||||
(eq (aref proof 0) token)
|
||||
(eq (aref proof 1) source)
|
||||
(listp source)
|
||||
(not (stringp source))
|
||||
(let ((children (ebox-tree--children-raw node)))
|
||||
(and (eq (car children) source)
|
||||
(null (cdr children))))
|
||||
(if (eq token ebox--flex-item-wrapper-adapter-token)
|
||||
(let ((content (plist-get source :ebox-content-node)))
|
||||
(and (eq (plist-get source :ebox-type) 'box)
|
||||
(listp content)
|
||||
(not (stringp content))
|
||||
(eq (aref proof 2) content)))
|
||||
(null (aref proof 2))))))
|
||||
|
||||
(defun ebox-tree--register-legacy-flex-item-adapter
|
||||
(adapter token source content)
|
||||
"Register ADAPTER's exact TOKEN, SOURCE, and optional CONTENT identities."
|
||||
(puthash adapter (vector token source content)
|
||||
ebox-tree--legacy-flex-item-adapter-proofs)
|
||||
adapter)
|
||||
|
||||
(defun ebox-tree--remint-legacy-flex-item-adapter-proof (old new)
|
||||
"Bind NEW adapter provenance to copied children when OLD is authentic."
|
||||
(cond
|
||||
((ebox-tree--legacy-flex-item-adapter-p
|
||||
old ebox--flex-item-direct-adapter-token)
|
||||
(ebox-tree--register-legacy-flex-item-adapter
|
||||
new ebox--flex-item-direct-adapter-token (plist-get new :node) nil))
|
||||
((ebox-tree--legacy-flex-item-adapter-p
|
||||
old ebox--flex-item-wrapper-adapter-token)
|
||||
(let ((wrapper (plist-get new :node)))
|
||||
(ebox-tree--register-legacy-flex-item-adapter
|
||||
new ebox--flex-item-wrapper-adapter-token
|
||||
wrapper (plist-get wrapper :ebox-content-node))))
|
||||
(t
|
||||
(remhash new ebox-tree--legacy-flex-item-adapter-proofs)
|
||||
new)))
|
||||
|
||||
(defun ebox-tree--legacy-flex-item-duplicate-p (node parent grandparent)
|
||||
"Return non-nil when NODE is an exact duplicate inside a legacy adapter."
|
||||
(or
|
||||
(and (ebox-tree--legacy-flex-item-adapter-p
|
||||
parent ebox--flex-item-direct-adapter-token)
|
||||
(eq node (plist-get parent :node)))
|
||||
(and (ebox-tree--legacy-flex-item-adapter-p
|
||||
parent ebox--flex-item-wrapper-adapter-token)
|
||||
(let ((wrapper (plist-get parent :node)))
|
||||
(and (eq node wrapper)
|
||||
(eq (plist-get wrapper :ebox-type) 'box)
|
||||
(listp (plist-get wrapper :ebox-content-node)))))
|
||||
(and (ebox-tree--legacy-flex-item-adapter-p
|
||||
grandparent ebox--flex-item-wrapper-adapter-token)
|
||||
(let ((wrapper (plist-get grandparent :node)))
|
||||
(and (eq parent wrapper)
|
||||
(eq (plist-get wrapper :ebox-type) 'box)
|
||||
(eq node (plist-get wrapper :ebox-content-node)))))))
|
||||
|
||||
(defun ebox-tree--validate-node-parent-participation
|
||||
(node parent-kind parent grandparent source)
|
||||
(node parent-kind source)
|
||||
"Validate NODE participation under PARENT-KIND from AUTHOR or COMPUTED SOURCE."
|
||||
(unless (ebox-tree--legacy-flex-item-duplicate-p node parent grandparent)
|
||||
(let ((keys (ebox-tree--node-participation-keys node source)))
|
||||
(when keys
|
||||
(when (eq (plist-get node :ebox-kind) 'text)
|
||||
(error "Ebox Text cannot carry participation properties: %S" keys))
|
||||
(pcase parent-kind
|
||||
('flex
|
||||
(when (cl-intersection
|
||||
keys ebox-tree--grid-participation-keys :test #'eq)
|
||||
(error "Grid participation requires a direct Grid parent: %S"
|
||||
keys)))
|
||||
('grid
|
||||
(when (cl-intersection
|
||||
keys ebox-tree--flex-participation-keys :test #'eq)
|
||||
(error "Flex participation requires a direct Flex parent: %S"
|
||||
keys)))
|
||||
(_ (error "Ebox participation requires a Flex or Grid parent: %S"
|
||||
keys))))))
|
||||
(let ((keys (ebox-tree--node-participation-keys node source)))
|
||||
(when keys
|
||||
(when (eq (plist-get node :ebox-kind) 'text)
|
||||
(error "Ebox Text cannot carry participation properties: %S" keys))
|
||||
(pcase parent-kind
|
||||
('flex
|
||||
(when (cl-intersection
|
||||
keys ebox-tree--grid-participation-keys :test #'eq)
|
||||
(error "Grid participation requires a direct Grid parent: %S"
|
||||
keys)))
|
||||
('grid
|
||||
(when (cl-intersection
|
||||
keys ebox-tree--flex-participation-keys :test #'eq)
|
||||
(error "Flex participation requires a direct Flex parent: %S"
|
||||
keys)))
|
||||
(_ (error "Ebox participation requires a Flex or Grid parent: %S"
|
||||
keys)))))
|
||||
node)
|
||||
|
||||
(defun ebox-tree--indexed-parent-context
|
||||
(node-id node-table parent-table)
|
||||
"Return (PARENT-KIND PARENT GRANDPARENT) for NODE-ID in indexed tree."
|
||||
"Return the direct parent layout kind for NODE-ID in indexed tree."
|
||||
(let* ((parent-id (gethash node-id parent-table))
|
||||
(parent (and parent-id (gethash parent-id node-table)))
|
||||
(grandparent-id (and parent-id (gethash parent-id parent-table)))
|
||||
(grandparent (and grandparent-id
|
||||
(gethash grandparent-id node-table)))
|
||||
(layout-owner parent))
|
||||
(while (and layout-owner
|
||||
(eq (plist-get layout-owner :ebox-type) 'flex-item))
|
||||
(let ((owner-id (plist-get layout-owner :node-id)))
|
||||
(setq owner-id (and owner-id (gethash owner-id parent-table))
|
||||
layout-owner (and owner-id (gethash owner-id node-table)))))
|
||||
(list (and layout-owner
|
||||
(ebox-tree--child-layout-kind layout-owner nil))
|
||||
parent grandparent)))
|
||||
(parent (and parent-id (gethash parent-id node-table))))
|
||||
(and parent (ebox-tree--child-layout-kind parent nil))))
|
||||
|
||||
(defun ebox-tree-validate-indexed-participation
|
||||
(node-table parent-table node-ids &optional source)
|
||||
@ -749,11 +737,10 @@ Host references are root-global opaque metadata and are compared with `equal'."
|
||||
(setq source (or source 'author))
|
||||
(dolist (node-id (delete-dups (copy-sequence node-ids)))
|
||||
(when-let* ((node (gethash node-id node-table)))
|
||||
(pcase-let ((`(,parent-kind ,parent ,grandparent)
|
||||
(ebox-tree--indexed-parent-context
|
||||
node-id node-table parent-table)))
|
||||
(ebox-tree--validate-node-parent-participation
|
||||
node parent-kind parent grandparent source))))
|
||||
(ebox-tree--validate-node-parent-participation
|
||||
node
|
||||
(ebox-tree--indexed-parent-context node-id node-table parent-table)
|
||||
source)))
|
||||
node-table)
|
||||
|
||||
(defun ebox-tree-validate-declarative-root
|
||||
@ -771,7 +758,7 @@ and all explicit identities are compared with `equal'. Return ROOT on success."
|
||||
(active (make-hash-table :test 'eq))
|
||||
(range-refs (make-hash-table :test 'equal)))
|
||||
(cl-labels
|
||||
((visit (node parent-kind parent grandparent)
|
||||
((visit (node parent-kind)
|
||||
(when (and (listp node) (not (stringp node)))
|
||||
(when (gethash node active)
|
||||
(error "Ebox declarative tree contains a cycle"))
|
||||
@ -781,37 +768,52 @@ and all explicit identities are compared with `equal'. Return ROOT on success."
|
||||
(puthash node t active)
|
||||
(when validate-participation-p
|
||||
(ebox-tree--validate-node-parent-participation
|
||||
node parent-kind parent grandparent 'author))
|
||||
node parent-kind 'author))
|
||||
(let* ((keys (make-hash-table :test 'equal))
|
||||
(type (plist-get node :ebox-type))
|
||||
(raw (and (memq type '(concat stack flex grid))
|
||||
(raw (and (memq type '(box concat stack flex grid))
|
||||
(plist-get node :children)))
|
||||
children)
|
||||
(when (and raw (not (proper-list-p raw)))
|
||||
(error "Ebox material children must be a proper list"))
|
||||
(dolist (entry (ebox-tree--children-raw node))
|
||||
(if (ebox-child-range--descriptor-p entry)
|
||||
(progn
|
||||
(unless (and (proper-list-p raw) (memq entry raw))
|
||||
(error "Ebox child Range descriptor is invalid in a scalar slot"))
|
||||
(let ((ref (ebox-child-range--descriptor-ref entry))
|
||||
(items (ebox-child-range--descriptor-items entry)))
|
||||
(unless (and ref (proper-list-p items))
|
||||
(error "Ebox child Range descriptor is invalid"))
|
||||
(when (gethash ref range-refs)
|
||||
(error "Ebox child Range ref is not unique: %S" ref))
|
||||
(puthash ref t range-refs)
|
||||
(dolist (item items)
|
||||
(when (ebox-child-range--descriptor-p item)
|
||||
(error "Nested child Range descriptors are reserved"))
|
||||
(unless (and (listp item) (not (stringp item)))
|
||||
(error "Ebox child Range item must be a node"))
|
||||
(push item children))))
|
||||
(progn
|
||||
(when (ebox-child-range--descriptor-p entry)
|
||||
(error "Ebox child Range descriptor is invalid here"))
|
||||
(when-let* ((sequence (plist-get node :ebox-child-sequence)))
|
||||
(dotimes (index
|
||||
(ebox-child-range--sequence-count sequence))
|
||||
(when-let* ((ref
|
||||
(ebox-child-range--segment-ref
|
||||
(ebox-child-range--segment-at sequence index))))
|
||||
(when (gethash ref range-refs)
|
||||
(error "Ebox child Range ref is not unique: %S" ref))
|
||||
(puthash ref t range-refs))))
|
||||
(if (plist-get node :ebox-child-sequence)
|
||||
(ebox-tree-for-each-direct-child
|
||||
node (lambda (child) (push child children)))
|
||||
(dolist (entry (ebox-tree--children-raw node))
|
||||
(if (ebox-child-range--descriptor-p entry)
|
||||
(progn
|
||||
(unless (and (proper-list-p raw) (memq entry raw))
|
||||
(error "Ebox child Range descriptor is invalid in a scalar slot"))
|
||||
(let ((ref (ebox-child-range--descriptor-ref entry))
|
||||
(items (ebox-child-range--descriptor-items entry)))
|
||||
(unless (and ref (proper-list-p items))
|
||||
(error "Ebox child Range descriptor is invalid"))
|
||||
(when (gethash ref range-refs)
|
||||
(error "Ebox child Range ref is not unique: %S" ref))
|
||||
(puthash ref t range-refs)
|
||||
(dolist (item items)
|
||||
(when (ebox-child-range--descriptor-p item)
|
||||
(error "Nested child Range descriptors are reserved"))
|
||||
(unless (and (listp item) (not (stringp item)))
|
||||
(error "Ebox child Range item must be a node"))
|
||||
(push item children))))
|
||||
(push entry children))))
|
||||
(dolist (child (nreverse children))
|
||||
(when (and (eq (plist-get node :ebox-kind) 'box)
|
||||
(not (memq (plist-get child :ebox-kind)
|
||||
'(text box))))
|
||||
(error
|
||||
"Canonical Ebox Box child must be Text or Box: %S"
|
||||
child))
|
||||
(when-let* ((key (and (listp child)
|
||||
(not (stringp child))
|
||||
(plist-get child :key))))
|
||||
@ -820,10 +822,9 @@ and all explicit identities are compared with `equal'. Return ROOT on success."
|
||||
key))
|
||||
(puthash key t keys))
|
||||
(visit child
|
||||
(ebox-tree--child-layout-kind node parent-kind)
|
||||
node parent)))
|
||||
(remhash node active))))
|
||||
(visit root root-parent-kind nil nil))
|
||||
(ebox-tree--child-layout-kind node parent-kind)))
|
||||
(remhash node active)))))
|
||||
(visit root root-parent-kind))
|
||||
root))
|
||||
|
||||
(defun ebox-tree-clear-runtime-identities (root)
|
||||
@ -840,14 +841,13 @@ and all explicit identities are compared with `equal'. Return ROOT on success."
|
||||
(plist-put node :surface-object nil))
|
||||
(when (plist-member node :ebox-sequence-location)
|
||||
(plist-put node :ebox-sequence-location nil))
|
||||
(dolist (child (ebox-tree--children-raw node))
|
||||
(visit child)))))
|
||||
(ebox-tree-for-each-direct-child node #'visit))))
|
||||
(visit root))
|
||||
root)
|
||||
|
||||
(defconst ebox-tree--runtime-source-keys
|
||||
'(:node-id :region-id :surface-object :render-cache :ebox-sequence-location
|
||||
:ebox-content-width-exact-p)
|
||||
:ebox-content-width-exact-p :ebox-content-layout-complete-p)
|
||||
"Runtime-owned plist keys excluded from declarative source signatures.")
|
||||
|
||||
(defconst ebox-tree--child-source-keys
|
||||
@ -858,13 +858,17 @@ and all explicit identities are compared with `equal'. Return ROOT on success."
|
||||
(defconst ebox-tree--excluded-source-keys
|
||||
(append ebox-tree--runtime-source-keys
|
||||
ebox-tree--child-source-keys
|
||||
'(:ebox-style-declarations :ebox-style-overrides
|
||||
'(:ebox-style-overrides
|
||||
:ebox-grid-config
|
||||
:ebox-computed-style :ebox-style-wrapper
|
||||
:ebox-style-generated-wrapper))
|
||||
"Non-rendering keys excluded from node-local source comparison.
|
||||
Cascade declarations and computed-style records are side state; their
|
||||
projected Ebox longhands carry the actual paint and layout consequences.")
|
||||
:ebox-style-generated-wrapper
|
||||
:ebox-author-style-count :ebox-author-style-required-p
|
||||
:ebox-text-value :ebox-candidate-computed-style-p))
|
||||
"Derived keys excluded from node-local source comparison.
|
||||
Canonical declarations remain source facts and therefore participate in diff;
|
||||
their property-level delta is normalized by the incremental candidate boundary.
|
||||
Computed styles, projection counters, duplicate Text storage, and legacy wrapper
|
||||
overrides are derived side state.")
|
||||
|
||||
(defconst ebox-tree-metadata-source-keys
|
||||
'(:key :id :class :host-ref)
|
||||
@ -1073,12 +1077,6 @@ candidate runtime index agree."
|
||||
(setq props (append props (list key (plist-get node key)))))))
|
||||
props))
|
||||
|
||||
(defun ebox-tree-flex-item-source-node (node)
|
||||
"Return the renderable source node for a flex item candidate."
|
||||
(if (and (listp node) (eq (plist-get node :ebox-type) 'flex-item))
|
||||
(plist-get node :node)
|
||||
node))
|
||||
|
||||
(defun ebox-tree--semantic-layout-leaves (node)
|
||||
"Return semantic leaves below internal layout adapter NODE."
|
||||
(pcase (and (listp node) (plist-get node :ebox-type))
|
||||
@ -1086,24 +1084,22 @@ candidate runtime index agree."
|
||||
(apply #'append
|
||||
(mapcar #'ebox-tree--semantic-layout-leaves
|
||||
(ebox-tree-layout-children node))))
|
||||
('flex-item
|
||||
(ebox-tree--semantic-layout-leaves (plist-get node :node)))
|
||||
(_ (and (listp node) (list node)))))
|
||||
|
||||
(defun ebox-tree-semantic-children (node)
|
||||
"Return NODE's logical children without internal layout adapters."
|
||||
(pcase (and (listp node) (plist-get node :ebox-type))
|
||||
('box
|
||||
(if (plist-member node :children)
|
||||
(plist-get node :children)
|
||||
(if (plist-get node :ebox-child-sequence)
|
||||
(ebox-tree-layout-children node)
|
||||
(if (plist-member node :children)
|
||||
(plist-get node :children)
|
||||
(when-let* ((content-node (plist-get node :ebox-content-node)))
|
||||
(ebox-tree--semantic-layout-leaves content-node))))
|
||||
(ebox-tree--semantic-layout-leaves content-node)))))
|
||||
((or 'concat 'stack)
|
||||
(ebox-tree--semantic-layout-leaves node))
|
||||
('flex
|
||||
(mapcar #'ebox-tree-flex-item-source-node (ebox-tree-layout-children node)))
|
||||
('flex (ebox-tree-layout-children node))
|
||||
('grid (ebox-tree-layout-children node))
|
||||
('flex-item (delq nil (list (plist-get node :node))))
|
||||
(_ (ebox-tree-children node))))
|
||||
|
||||
(defun ebox-tree-selector-index (root)
|
||||
@ -1184,14 +1180,8 @@ candidate runtime index agree."
|
||||
|
||||
(defun ebox-tree-flex-participation-props (node)
|
||||
"Return raw flex item participation props attached to NODE."
|
||||
(cond
|
||||
((and (listp node) (eq (plist-get node :ebox-type) 'flex-item))
|
||||
(append (plist-get node :props)
|
||||
(ebox-tree-flex-direct-participation-props
|
||||
(plist-get node :node))))
|
||||
((listp node)
|
||||
(ebox-tree-flex-direct-participation-props node))
|
||||
(t nil)))
|
||||
(and (listp node)
|
||||
(ebox-tree-flex-direct-participation-props node)))
|
||||
|
||||
(defun ebox-tree-node-all-region-ids (node)
|
||||
"Return every box region id contained by NODE.
|
||||
@ -1238,8 +1228,6 @@ not only the public update ids."
|
||||
(apply #'append
|
||||
(mapcar #'ebox-tree-node-all-region-ids
|
||||
(ebox-tree-layout-children node)))))
|
||||
('flex-item
|
||||
(ebox-tree-node-all-region-ids (plist-get node :node)))
|
||||
(_ nil))))
|
||||
(when ebox--node-region-ids-cache
|
||||
(puthash node region-ids ebox--node-region-ids-cache))
|
||||
@ -1284,9 +1272,6 @@ cannot be inspected and reports no conflict."
|
||||
(cl-some (lambda (child)
|
||||
(ebox-tree-node-paint-conflict-p child paint-key t))
|
||||
(ebox-tree-layout-children node))))
|
||||
('flex-item
|
||||
(ebox-tree-node-paint-conflict-p
|
||||
(plist-get node :node) paint-key inherited))
|
||||
(_ nil)))))
|
||||
|
||||
(defun ebox-tree-node-path-to-region (node region-id)
|
||||
@ -1341,14 +1326,16 @@ wrapper's visible content is produced by the flex renderer."
|
||||
(when-let* ((path (ebox-tree-node-path-to-region child region-id)))
|
||||
(throw 'found (append path (list node)))))
|
||||
nil))))
|
||||
('flex-item
|
||||
(ebox-tree-node-path-to-region (plist-get node :node) region-id))
|
||||
(_ nil)))))
|
||||
|
||||
(defun ebox-tree-node-visible-overflow-p (node)
|
||||
"Return non-nil when NODE contains visible overflow that cannot be span patched."
|
||||
(cond
|
||||
((or (stringp node) (not (listp node))) nil)
|
||||
;; Canonical Text temporarily shares the legacy `ebox-type' storage tag so
|
||||
;; the old renderer can paint it. It is nevertheless a leaf, not a Box
|
||||
;; formatting context, and therefore cannot own geometric overflow.
|
||||
((eq (plist-get node :ebox-kind) 'text) nil)
|
||||
(t
|
||||
(pcase (plist-get node :ebox-type)
|
||||
('box
|
||||
@ -1371,20 +1358,18 @@ wrapper's visible content is produced by the flex renderer."
|
||||
(ebox--box-visible-overflow-p (plist-get node :box)))
|
||||
(cl-some #'ebox-tree-node-visible-overflow-p
|
||||
(ebox-tree-layout-children node))))
|
||||
('flex-item
|
||||
(ebox-tree-node-visible-overflow-p (plist-get node :node)))
|
||||
(_ nil)))))
|
||||
|
||||
(defalias 'ebox--computed-display #'ebox-tree-computed-display)
|
||||
(defalias 'ebox--display-outer #'ebox-tree-display-outer)
|
||||
(defalias 'ebox--display-inner #'ebox-tree-display-inner)
|
||||
(defalias 'ebox--formatting-context-p #'ebox-tree-formatting-context-p)
|
||||
(defalias 'ebox--layout-props #'ebox-tree-layout-props)
|
||||
(defalias 'ebox--node-selector-type #'ebox-tree-node-selector-type)
|
||||
(defalias 'ebox--layout-children #'ebox-tree-layout-children)
|
||||
(defalias 'ebox--node-children #'ebox-tree-children)
|
||||
(defalias 'ebox--flex-direct-participation-props
|
||||
#'ebox-tree-flex-direct-participation-props)
|
||||
(defalias 'ebox--flex-item-source-node #'ebox-tree-flex-item-source-node)
|
||||
(defalias 'ebox--flex-participation-props
|
||||
#'ebox-tree-flex-participation-props)
|
||||
(defalias 'ebox--node-all-region-ids #'ebox-tree-node-all-region-ids)
|
||||
|
||||
102
ebox.el
102
ebox.el
@ -432,6 +432,7 @@ publication."
|
||||
|
||||
(defun ebox--ensure-region-id (box)
|
||||
"Return BOX's stable region id, creating one if needed."
|
||||
(ebox--ensure-node-id box)
|
||||
(or (ebox-get box :region-id)
|
||||
(let ((id (ebox--next-region-id)))
|
||||
(ebox-put box :region-id id)
|
||||
@ -632,18 +633,7 @@ ALIGN can be `top', `center', or `bottom'."
|
||||
"Return NODE's region ids in document order.
|
||||
NODE can be an unrendered layout tree or an already-rendered string.
|
||||
For layout trees, missing ids are assigned to box plists and remain stable
|
||||
for later renders and dynamic updates.
|
||||
|
||||
Use this to capture ids *before* inserting into a buffer:
|
||||
|
||||
(let* ((layout (ebox-stack info-box (ebox-concat left right)))
|
||||
(ids (ebox-region-ids layout))
|
||||
(info-id (nth 0 ids))
|
||||
(left-id (nth 1 ids))
|
||||
(right-id (nth 2 ids)))
|
||||
(ebox-render-to-buffer \"*my-ui*\" layout)
|
||||
;; now use info-id / left-id / right-id for dynamic updates
|
||||
)"
|
||||
for later renders and dynamic updates."
|
||||
(cond
|
||||
((stringp node)
|
||||
(ebox--string-region-ids node))
|
||||
@ -677,8 +667,6 @@ Use this to capture ids *before* inserting into a buffer:
|
||||
(apply #'append
|
||||
(mapcar #'ebox-region-ids
|
||||
(ebox-tree-layout-children node)))))
|
||||
((eq type 'flex-item)
|
||||
(ebox-region-ids (plist-get node :node)))
|
||||
(t nil))))))
|
||||
|
||||
(defun ebox--runtime-node-ids (node)
|
||||
@ -3796,9 +3784,10 @@ With prefix ARG, scroll by that many content lines."
|
||||
(defun ebox--clear-buffer-runtime-state (&optional buffer)
|
||||
"Remove all runtime caches owned by BUFFER.
|
||||
Defaults to the current buffer."
|
||||
(let ((buffer (or buffer (current-buffer))))
|
||||
(let* ((buffer (or buffer (current-buffer)))
|
||||
(state (ebox--buffer-render-state buffer)))
|
||||
(ebox--cancel-buffer-runtime-prewarm buffer)
|
||||
(when-let* ((root (ebox--buffer-root-node buffer)))
|
||||
(when-let* ((root (plist-get state :root-node)))
|
||||
(ebox--clear-region-runtime-caches
|
||||
(ebox--node-all-region-ids root)))
|
||||
(ebox--clear-buffer-render-state buffer)))
|
||||
@ -3849,6 +3838,19 @@ Defaults to the current buffer."
|
||||
(:background-color :bgcolor)
|
||||
(_ property)))
|
||||
|
||||
(defun ebox--region-update-direct-text-node (box)
|
||||
"Return BOX's sole direct canonical Text child, or nil."
|
||||
(let ((children (and (ebox-box-node-p box)
|
||||
(ebox-box-node-children box))))
|
||||
(and (= (length children) 1)
|
||||
(ebox-text-node-p (car children))
|
||||
(car children))))
|
||||
|
||||
(defun ebox--region-update-content-value (box)
|
||||
"Return the mutable region content represented by canonical BOX."
|
||||
(when-let* ((text (ebox--region-update-direct-text-node box)))
|
||||
(ebox-text-node-value text)))
|
||||
|
||||
(defun ebox--region-update-analysis (box expanded declarations)
|
||||
"Return change analysis for BOX, EXPANDED fields, and DECLARATIONS."
|
||||
(let (content-seen
|
||||
@ -3867,12 +3869,18 @@ Defaults to the current buffer."
|
||||
(setq style-changed-p t)))
|
||||
(t
|
||||
(push key unknown-keys))))
|
||||
(when (and content-seen (not (stringp content-value)))
|
||||
(user-error "Ebox content update requires a string"))
|
||||
(when (and content-seen
|
||||
(not (ebox--region-update-direct-text-node box)))
|
||||
(user-error "Ebox content update requires one direct Text child"))
|
||||
(list :changed-p
|
||||
(or (not (equal declarations
|
||||
(plist-get box :ebox-style-declarations)))
|
||||
style-changed-p
|
||||
(and content-seen
|
||||
(not (equal (ebox-get box :content) content-value))))
|
||||
(not (equal (ebox--region-update-content-value box)
|
||||
content-value))))
|
||||
:unknown-keys (nreverse unknown-keys))))
|
||||
|
||||
(defun ebox--schedule-runtime-prewarm-after-batch-flush
|
||||
@ -3900,7 +3908,7 @@ cannot be changed through the unpublished batch root."
|
||||
(ebox-tree-copy-node-structure batch-root))
|
||||
(and path-copy-p
|
||||
(ebox-incremental-surface-region-candidate-root
|
||||
buffer region-id))
|
||||
buffer region-id t))
|
||||
(ebox-tree-copy-node-structure (plist-get state :root-node)))))
|
||||
|
||||
(defun ebox--surface-region-apply-props
|
||||
@ -3910,18 +3918,28 @@ cannot be changed through the unpublished batch root."
|
||||
(unless box
|
||||
(user-error "Ebox region handle no longer resolves to a box"))
|
||||
(let ((declarations-changed
|
||||
(not (equal declarations
|
||||
(plist-get box :ebox-style-declarations)))))
|
||||
(not (ebox-style-declarations-equal-p
|
||||
declarations
|
||||
(plist-get box :ebox-style-declarations)))))
|
||||
(when declarations-changed
|
||||
(plist-put box :ebox-style-declarations declarations))
|
||||
(cl-loop for (key value) on expanded by #'cddr
|
||||
for target-key = (ebox--region-update-normalize-property key)
|
||||
when (or (eq target-key :content)
|
||||
(memq target-key ebox--region-update-longhand-props))
|
||||
when (or (not (equal (ebox-get box target-key) value))
|
||||
when (or (not (equal (if (eq target-key :content)
|
||||
(ebox--region-update-content-value box)
|
||||
(ebox-get box target-key))
|
||||
value))
|
||||
(and declarations-changed
|
||||
(ebox-style-property key)))
|
||||
do (plist-put box target-key value)
|
||||
do (if (eq target-key :content)
|
||||
(if-let* ((text
|
||||
(ebox--region-update-direct-text-node box)))
|
||||
(plist-put text :ebox-text-value value)
|
||||
(user-error
|
||||
"Ebox content update requires one direct Text child"))
|
||||
(plist-put box target-key value))
|
||||
and do (push target-key changed-keys)))
|
||||
(nreverse changed-keys)))
|
||||
|
||||
@ -4176,7 +4194,7 @@ REGION-ID and CHANGED-KEYS describe one update; CHANGES describes a batch."
|
||||
:content)
|
||||
(stringp (cadr expanded))
|
||||
(numberp (ebox-get candidate-box :width))
|
||||
(null (ebox-tree-node-children candidate-box))
|
||||
(ebox--region-update-direct-text-node candidate-box)
|
||||
(not (ebox-style-cascade-active-p))
|
||||
(not (plist-get state :cascade-required-p))
|
||||
(null (plist-get state :scroll-region-ids))))
|
||||
@ -4296,8 +4314,7 @@ Examples:
|
||||
;;;###autoload
|
||||
(defun ebox-render-to-buffer (buffer-or-name node)
|
||||
"Render layout NODE into BUFFER-OR-NAME and return the buffer.
|
||||
NODE can be any box/concat/stack node created by `ebox-create',
|
||||
`ebox-concat', or `ebox-stack', or a pre-rendered string."
|
||||
NODE is a typed TextNode/BoxNode or a pre-rendered string."
|
||||
(declare (indent 1))
|
||||
(unless (stringp node)
|
||||
;; Validate opaque host identities before preparing a target generation.
|
||||
@ -4591,22 +4608,15 @@ through dirty-set and patch-set execution before falling back to root rerender."
|
||||
ebox-candidate-replace-host-ref
|
||||
ebox-candidate-patch-host-paint
|
||||
ebox-clear-cache
|
||||
ebox-column
|
||||
ebox-column-layout-create
|
||||
ebox-normal-layout-create
|
||||
ebox-row-layout-create
|
||||
ebox-text-create
|
||||
ebox-box-create
|
||||
ebox-commit
|
||||
ebox-concat
|
||||
ebox-create
|
||||
ebox-display-buffer
|
||||
ebox-display-signature
|
||||
ebox-flex
|
||||
ebox-flex-layout-create
|
||||
ebox-flex-item
|
||||
ebox-grid
|
||||
ebox-grid-fr
|
||||
ebox-grid-layout-create
|
||||
ebox-host-ref-bounds
|
||||
ebox-host-ref-position
|
||||
@ -4620,7 +4630,6 @@ through dirty-set and patch-set execution before falling back to root rerender."
|
||||
ebox-render-burst-end
|
||||
ebox-render-to-buffer
|
||||
ebox-rerender-buffer-with-context
|
||||
ebox-row
|
||||
ebox-scroll-down
|
||||
ebox-scroll-map
|
||||
ebox-scroll-page-down
|
||||
@ -4633,8 +4642,6 @@ through dirty-set and patch-set execution before falling back to root rerender."
|
||||
ebox-selector-query-all
|
||||
ebox-selector-query-buffer
|
||||
ebox-selector-update-buffer
|
||||
ebox-spacer
|
||||
ebox-stack
|
||||
ebox-string-pixel-width
|
||||
ebox-update-selector
|
||||
ebox-viewport-window-width
|
||||
@ -4684,29 +4691,10 @@ and are documented separately in the public API reference.")
|
||||
;; ebox-normal-layout-create
|
||||
;; Create the typed Normal LayoutConfig.
|
||||
;; ebox-box-create :layout config :children nodes &rest geometry-props
|
||||
;; Create one typed canonical BoxNode. The current vertical slice
|
||||
;; accepts Normal layout with zero or one canonical child.
|
||||
;; ebox-create &rest plist
|
||||
;; Legacy untyped runtime constructor retained during migration.
|
||||
;; Key options: :content :width :height
|
||||
;; :padding :margin :border :border-color :bgcolor
|
||||
;; :text-align :wrap-mode :box-sizing :surface-properties :host-ref
|
||||
;; Create one typed canonical BoxNode.
|
||||
;; ebox-build dsl
|
||||
;; Compile ETML-style (box/row/column/flex/spacer ...) forms into
|
||||
;; normal ebox layout nodes.
|
||||
;;
|
||||
;; ── Layout ────────────────────────────────────────────────────
|
||||
;; ebox-concat left right → horizontal side-by-side
|
||||
;; ebox-stack top bottom → vertical stacking (nest for 3+)
|
||||
;; ebox-row &rest nodes → horizontal layout from many nodes
|
||||
;; ebox-column &rest nodes → vertical layout from many nodes
|
||||
;; ebox-spacer &rest plist → blank box node
|
||||
;; ebox-flex &rest items → flex layout from boxes/items
|
||||
;; Container may also carry normal ebox-create box properties.
|
||||
;; ebox-flex-item node &rest plist
|
||||
;; Attach :order :flex-grow :flex-shrink :flex-basis :flex
|
||||
;; and :align-self metadata to a child node; non-flex properties
|
||||
;; wrap the child in an ebox-create box.
|
||||
;; Compile String/text/box/row/column/flex/grid author forms into
|
||||
;; typed TextNode/BoxNode values.
|
||||
;;
|
||||
;; ── Rendering ─────────────────────────────────────────────────
|
||||
;; ebox-render box → propertized string (no buffer side-effect)
|
||||
|
||||
@ -230,14 +230,15 @@ fn push_edit(
|
||||
});
|
||||
}
|
||||
|
||||
fn coordinate_edits<T, F>(old: &[T], new: &[T], text_equal: &F) -> Vec<SpanEdit>
|
||||
where
|
||||
F: Fn(&T, &T) -> bool,
|
||||
{
|
||||
fn coordinate_edits(
|
||||
old_len: usize,
|
||||
new_len: usize,
|
||||
text_matches: &[(usize, usize, usize)],
|
||||
) -> Vec<SpanEdit> {
|
||||
let mut old_position = 0;
|
||||
let mut new_position = 0;
|
||||
let mut edits = Vec::new();
|
||||
for (matched_old, matched_new, length) in matches(old, new, text_equal) {
|
||||
for &(matched_old, matched_new, length) in text_matches {
|
||||
if old_position < matched_old || new_position < matched_new {
|
||||
push_edit(
|
||||
&mut edits,
|
||||
@ -253,28 +254,26 @@ where
|
||||
push_edit(
|
||||
&mut edits,
|
||||
old_position,
|
||||
old.len(),
|
||||
old_len,
|
||||
new_position,
|
||||
new.len(),
|
||||
new_len,
|
||||
);
|
||||
edits
|
||||
}
|
||||
|
||||
fn semantic_edits<T, FText, FSemantic>(
|
||||
fn semantic_edits<T, FSemantic>(
|
||||
old: &[T],
|
||||
new: &[T],
|
||||
text_equal: &FText,
|
||||
text_matches: &[(usize, usize, usize)],
|
||||
semantic_equal: &FSemantic,
|
||||
) -> Vec<SpanEdit>
|
||||
where
|
||||
FText: Fn(&T, &T) -> bool,
|
||||
FSemantic: Fn(&T, &T) -> bool,
|
||||
{
|
||||
let text_matches = matches(old, new, text_equal);
|
||||
let mut old_position = 0;
|
||||
let mut new_position = 0;
|
||||
let mut edits = Vec::new();
|
||||
for (matched_old, matched_new, length) in text_matches {
|
||||
for &(matched_old, matched_new, length) in text_matches {
|
||||
if old_position < matched_old || new_position < matched_new {
|
||||
push_edit(
|
||||
&mut edits,
|
||||
@ -343,13 +342,14 @@ where
|
||||
target: target_revision,
|
||||
});
|
||||
}
|
||||
let text_matches = matches(old, new, &text_equal);
|
||||
Ok(CommitBatch {
|
||||
base_revision,
|
||||
target_revision,
|
||||
base_extent: old.len(),
|
||||
target_extent: new.len(),
|
||||
semantic_edits: semantic_edits(old, new, &text_equal, &semantic_equal),
|
||||
coordinate_edits: coordinate_edits(old, new, &text_equal),
|
||||
semantic_edits: semantic_edits(old, new, &text_matches, &semantic_equal),
|
||||
coordinate_edits: coordinate_edits(old.len(), new.len(), &text_matches),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -121,6 +121,14 @@ enum LayoutNode {
|
||||
Box {
|
||||
region_id: i64,
|
||||
content: Option<Box<MeasuredText>>,
|
||||
#[serde(default)]
|
||||
content_region_id: Option<i64>,
|
||||
#[serde(default)]
|
||||
content_typography_style: Option<u32>,
|
||||
#[serde(default)]
|
||||
content_foreground_style: Option<u32>,
|
||||
#[serde(default)]
|
||||
content_surface_template_id: Option<u32>,
|
||||
child: Option<Box<LayoutNode>>,
|
||||
content_width_exact: bool,
|
||||
#[serde(default)]
|
||||
@ -142,6 +150,8 @@ enum LayoutNode {
|
||||
margin_bottom: i64,
|
||||
border_left: i64,
|
||||
border_right: i64,
|
||||
#[serde(default)]
|
||||
typography_style: Option<u32>,
|
||||
foreground_style: Option<u32>,
|
||||
background_style: Option<u32>,
|
||||
border_left_style: Option<u32>,
|
||||
@ -156,6 +166,16 @@ enum LayoutNode {
|
||||
wrap_mode: WrapMode,
|
||||
scroll_offset: i64,
|
||||
},
|
||||
Text {
|
||||
region_id: i64,
|
||||
content: Box<MeasuredText>,
|
||||
#[serde(default)]
|
||||
typography_style: Option<u32>,
|
||||
foreground_style: Option<u32>,
|
||||
#[serde(default)]
|
||||
surface_template_id: Option<u32>,
|
||||
wrap_mode: WrapMode,
|
||||
},
|
||||
Row {
|
||||
children: Vec<LayoutNode>,
|
||||
},
|
||||
@ -1249,6 +1269,9 @@ fn composed_face(
|
||||
list_value = false;
|
||||
}
|
||||
StyleMode::Add => {
|
||||
if faces.len() == 1 && styles[faces[0]].face == style.face {
|
||||
continue;
|
||||
}
|
||||
if !faces.is_empty() {
|
||||
list_value = true;
|
||||
}
|
||||
@ -2441,6 +2464,7 @@ fn encode_patch_combined_payload(
|
||||
/// Emacs adapter payload built on the editor-independent core batch.
|
||||
struct EmacsCommitBatch {
|
||||
core: CommitBatch,
|
||||
publication_edits: Vec<SpanEdit>,
|
||||
combined_payload: Vec<u8>,
|
||||
metadata_records: usize,
|
||||
fragment_bytes: Vec<u8>,
|
||||
@ -2451,6 +2475,24 @@ struct EmacsCommitBatch {
|
||||
fragment_style_delta: Option<FragmentStyleDelta>,
|
||||
}
|
||||
|
||||
fn coalesce_publication_edits(edits: &[SpanEdit]) -> Vec<SpanEdit> {
|
||||
const MAX_UNCHANGED_GAP: usize = 1;
|
||||
let mut result: Vec<SpanEdit> = Vec::with_capacity(edits.len());
|
||||
for &edit in edits {
|
||||
if let Some(previous) = result.last_mut() {
|
||||
let old_gap = edit.old_start.saturating_sub(previous.old_end);
|
||||
let new_gap = edit.new_start.saturating_sub(previous.new_end);
|
||||
if old_gap == new_gap && old_gap <= MAX_UNCHANGED_GAP {
|
||||
previous.old_end = edit.old_end;
|
||||
previous.new_end = edit.new_end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(edit);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn build_emacs_commit_batch(
|
||||
old: &FlatLayoutTape,
|
||||
target: &FlatLayoutTape,
|
||||
@ -2460,8 +2502,8 @@ fn build_emacs_commit_batch(
|
||||
max_bytes: usize,
|
||||
) -> Result<EmacsCommitBatch, String> {
|
||||
let core = tape_commit_batch(&old.characters, &target.characters);
|
||||
let descriptor_bytes = core
|
||||
.semantic_edits
|
||||
let publication_edits = coalesce_publication_edits(&core.semantic_edits);
|
||||
let descriptor_bytes = publication_edits
|
||||
.len()
|
||||
.checked_add(core.coordinate_edits.len())
|
||||
.ok_or_else(|| "Native layout patch descriptor count overflow".to_owned())?
|
||||
@ -2523,7 +2565,7 @@ fn build_emacs_commit_batch(
|
||||
};
|
||||
let replacements = encode_patch_replacement_body(
|
||||
&target.characters,
|
||||
&core.semantic_edits,
|
||||
&publication_edits,
|
||||
compiled_styles,
|
||||
payload_limit,
|
||||
)?;
|
||||
@ -2552,6 +2594,7 @@ fn build_emacs_commit_batch(
|
||||
};
|
||||
Ok(EmacsCommitBatch {
|
||||
core,
|
||||
publication_edits,
|
||||
combined_payload,
|
||||
metadata_records,
|
||||
fragment_bytes,
|
||||
@ -2588,7 +2631,7 @@ pub(crate) fn encode_layout_patch_tape(
|
||||
root_metadata,
|
||||
max_bytes,
|
||||
)?;
|
||||
let patch_count = count_u32(batch.core.semantic_edits.len(), "patches")?;
|
||||
let patch_count = count_u32(batch.publication_edits.len(), "patches")?;
|
||||
let coordinate_patch_count =
|
||||
count_u32(batch.core.coordinate_edits.len(), "coordinate patches")?;
|
||||
let target_character_count = u64::try_from(target.characters.len())
|
||||
@ -2598,9 +2641,10 @@ pub(crate) fn encode_layout_patch_tape(
|
||||
let mut writer = TapeWriter::new(max_bytes)?;
|
||||
writer.push_u64(base_character_count)?;
|
||||
writer.push_u32(patch_count)?;
|
||||
// Publication remains a minimal text/property patch. The compact target
|
||||
// role sidecar lets Emacs swap exact runtime indexes without synchronously
|
||||
// rescanning the page or rebuilding thousands of markers per frame.
|
||||
// Publication coalesces only one-character equal-coordinate gaps. The
|
||||
// pure core retains its exact minimal semantic edits, while the compact
|
||||
// target role sidecar lets Emacs swap exact runtime indexes without
|
||||
// synchronously rescanning the page or rebuilding thousands of markers.
|
||||
writer.push_u32(
|
||||
u32::from(batch.reuse_fragment_template)
|
||||
| (u32::from(batch.reuse_ownership_template) << 1)
|
||||
@ -2625,7 +2669,7 @@ pub(crate) fn encode_layout_patch_tape(
|
||||
)?;
|
||||
writer.push_u32(coordinate_patch_count)?;
|
||||
writer.push_u32(0)?;
|
||||
for patch in &batch.core.semantic_edits {
|
||||
for patch in &batch.publication_edits {
|
||||
writer.push_u64(patch.old_start as u64)?;
|
||||
writer.push_u64(patch.old_end as u64)?;
|
||||
writer.push_u64(patch.new_start as u64)?;
|
||||
@ -2991,6 +3035,7 @@ fn add_context_work(
|
||||
work_units: &mut usize,
|
||||
) -> Result<(), String> {
|
||||
match node {
|
||||
LayoutNode::Text { .. } => {}
|
||||
LayoutNode::Box {
|
||||
child,
|
||||
height,
|
||||
@ -3063,9 +3108,59 @@ fn validate_node(
|
||||
}
|
||||
add_work_units(work_units, 1)?;
|
||||
match node {
|
||||
LayoutNode::Text {
|
||||
region_id,
|
||||
content,
|
||||
typography_style,
|
||||
foreground_style,
|
||||
surface_template_id,
|
||||
..
|
||||
} => {
|
||||
if *region_id <= 0 {
|
||||
return Err("Native layout Text region id must be positive".to_owned());
|
||||
}
|
||||
for style_id in [typography_style, foreground_style].into_iter().flatten() {
|
||||
if *style_id >= style_count {
|
||||
return Err("Native layout Text style id exceeds the style table".to_owned());
|
||||
}
|
||||
}
|
||||
if let Some(template_id) = surface_template_id {
|
||||
if *template_id >= property_template_count {
|
||||
return Err(
|
||||
"Native layout Text property template id exceeds the property template table"
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if content.lines.is_empty() {
|
||||
return Err("Native layout Text must contain one line".to_owned());
|
||||
}
|
||||
add_work_units(work_units, content.lines.len())?;
|
||||
for line in &content.lines {
|
||||
add_work_units(work_units, line.clusters.len())?;
|
||||
for cluster in &line.clusters {
|
||||
if cluster.text.is_empty() {
|
||||
return Err("Native layout Text cluster cannot be empty".to_owned());
|
||||
}
|
||||
validate_dimension("Text cluster width", cluster.width)?;
|
||||
if let Some(template_id) = cluster.source_template_id {
|
||||
if template_id >= property_template_count {
|
||||
return Err(
|
||||
"Native layout Text property template id exceeds the property template table"
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LayoutNode::Box {
|
||||
region_id,
|
||||
content,
|
||||
content_region_id,
|
||||
content_typography_style,
|
||||
content_foreground_style,
|
||||
content_surface_template_id,
|
||||
child,
|
||||
content_width_exact: _,
|
||||
content_min_width,
|
||||
@ -3085,6 +3180,7 @@ fn validate_node(
|
||||
margin_bottom,
|
||||
border_left,
|
||||
border_right,
|
||||
typography_style,
|
||||
foreground_style,
|
||||
background_style,
|
||||
border_left_style,
|
||||
@ -3106,6 +3202,14 @@ fn validate_node(
|
||||
"Native layout box must contain exactly one text or child value".to_owned(),
|
||||
);
|
||||
}
|
||||
if content_region_id.is_some() && content.is_none() {
|
||||
return Err("Native layout box content identity requires measured text".to_owned());
|
||||
}
|
||||
if let Some(content_region_id) = content_region_id {
|
||||
if *content_region_id <= 0 {
|
||||
return Err("Native layout box content region id must be positive".to_owned());
|
||||
}
|
||||
}
|
||||
for (name, size) in [
|
||||
("width", width),
|
||||
("min-width", min_width),
|
||||
@ -3140,6 +3244,9 @@ fn validate_node(
|
||||
.unwrap_or(usize::MAX),
|
||||
)?;
|
||||
for style_id in [
|
||||
content_typography_style,
|
||||
content_foreground_style,
|
||||
typography_style,
|
||||
foreground_style,
|
||||
background_style,
|
||||
border_left_style,
|
||||
@ -3162,6 +3269,14 @@ fn validate_node(
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(template_id) = content_surface_template_id {
|
||||
if *template_id >= property_template_count {
|
||||
return Err(
|
||||
"Native layout box content property template id exceeds the property template table"
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(text) = content {
|
||||
if text.lines.is_empty() {
|
||||
return Err("Native layout measured text must contain one line".to_owned());
|
||||
@ -4660,6 +4775,7 @@ fn slice_rendered(rendered: Rendered, start: i64, height: i64) -> Rendered {
|
||||
|
||||
fn exact_rendered_height(node: &LayoutNode, context: LayoutContext) -> Option<i64> {
|
||||
match node {
|
||||
LayoutNode::Text { content, .. } => i64::try_from(content.lines.len()).ok(),
|
||||
LayoutNode::Box {
|
||||
height,
|
||||
min_height,
|
||||
@ -5158,6 +5274,10 @@ fn render_node_with_override(
|
||||
LayoutNode::Box {
|
||||
region_id,
|
||||
content,
|
||||
content_region_id,
|
||||
content_typography_style,
|
||||
content_foreground_style,
|
||||
content_surface_template_id,
|
||||
child,
|
||||
content_width_exact,
|
||||
content_min_width: _,
|
||||
@ -5178,6 +5298,7 @@ fn render_node_with_override(
|
||||
margin_bottom,
|
||||
border_left,
|
||||
border_right,
|
||||
typography_style,
|
||||
foreground_style,
|
||||
background_style,
|
||||
border_left_style,
|
||||
@ -5439,7 +5560,16 @@ fn render_node_with_override(
|
||||
}
|
||||
|
||||
let mut formatted = if let Some(text) = content {
|
||||
measured_lines(text, content_width, *wrap_mode)
|
||||
let mut content_lines = measured_lines(text, content_width, *wrap_mode);
|
||||
if let Some(content_region_id) = content_region_id {
|
||||
for (index, line) in content_lines.iter_mut().enumerate() {
|
||||
line.own_content(*content_region_id, index as i64);
|
||||
line.apply_style(*content_typography_style);
|
||||
line.apply_style(*content_foreground_style);
|
||||
line.apply_property_template(*content_surface_template_id);
|
||||
}
|
||||
}
|
||||
content_lines
|
||||
.into_iter()
|
||||
.map(|line| line.padded(content_width, *text_align))
|
||||
.collect::<Vec<_>>()
|
||||
@ -5580,6 +5710,7 @@ fn render_node_with_override(
|
||||
*padding_right,
|
||||
region_properties(RegionRole::PaddingRight, *region_id, None),
|
||||
);
|
||||
line.apply_style(*typography_style);
|
||||
line.apply_style(*foreground_style);
|
||||
line.apply_style(*background_style);
|
||||
line.prepend_space_with_properties(
|
||||
@ -5648,6 +5779,24 @@ fn render_node_with_override(
|
||||
}
|
||||
Ok(rendered)
|
||||
}
|
||||
LayoutNode::Text {
|
||||
region_id,
|
||||
content,
|
||||
typography_style,
|
||||
foreground_style,
|
||||
surface_template_id,
|
||||
wrap_mode,
|
||||
} => {
|
||||
let width = measured_max_width(content);
|
||||
let mut lines = measured_lines(content, width, *wrap_mode);
|
||||
for (index, line) in lines.iter_mut().enumerate() {
|
||||
line.own_content(*region_id, index as i64);
|
||||
line.apply_style(*typography_style);
|
||||
line.apply_style(*foreground_style);
|
||||
line.apply_property_template(*surface_template_id);
|
||||
}
|
||||
Ok(Rendered::from_lines(lines))
|
||||
}
|
||||
LayoutNode::Row { children } => {
|
||||
let child_context = LayoutContext {
|
||||
inline_auto_width_intrinsic: true,
|
||||
@ -5801,6 +5950,10 @@ mod tests {
|
||||
LayoutNode::Box {
|
||||
region_id,
|
||||
content: Some(Box::new(content)),
|
||||
content_region_id: None,
|
||||
content_typography_style: None,
|
||||
content_foreground_style: None,
|
||||
content_surface_template_id: None,
|
||||
child: None,
|
||||
content_width_exact: false,
|
||||
content_min_width: None,
|
||||
@ -5821,6 +5974,7 @@ mod tests {
|
||||
margin_bottom: 0,
|
||||
border_left: 0,
|
||||
border_right: 0,
|
||||
typography_style: None,
|
||||
foreground_style: None,
|
||||
background_style: None,
|
||||
border_left_style: None,
|
||||
@ -5857,6 +6011,10 @@ mod tests {
|
||||
LayoutNode::Box {
|
||||
region_id,
|
||||
content: None,
|
||||
content_region_id: None,
|
||||
content_typography_style: None,
|
||||
content_foreground_style: None,
|
||||
content_surface_template_id: None,
|
||||
child: Some(Box::new(child)),
|
||||
content_width_exact: true,
|
||||
content_min_width: None,
|
||||
@ -5877,6 +6035,7 @@ mod tests {
|
||||
margin_bottom: 0,
|
||||
border_left: 0,
|
||||
border_right: 0,
|
||||
typography_style: None,
|
||||
foreground_style: None,
|
||||
background_style: None,
|
||||
border_left_style: None,
|
||||
@ -5956,6 +6115,80 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_column_lowering_stretches_centered_child() {
|
||||
let mut text = auto_text_box(3, measured_text(vec![vec![cluster("x", 10, None)]]), None);
|
||||
let LayoutNode::Box { width, .. } = &mut text else {
|
||||
unreachable!();
|
||||
};
|
||||
*width = Size::MaxContent;
|
||||
let mut label = child_box(2, text, None);
|
||||
let LayoutNode::Box {
|
||||
width,
|
||||
min_width,
|
||||
content_width_exact,
|
||||
text_align,
|
||||
..
|
||||
} = &mut label
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
*width = Size::Auto;
|
||||
*min_width = Size::MaxContent;
|
||||
*content_width_exact = false;
|
||||
*text_align = HorizontalAlign::Center;
|
||||
let column = LayoutNode::Flex {
|
||||
direction: FlexDirection::Column,
|
||||
wrap: FlexWrap::Nowrap,
|
||||
justify: FlexAlign::FlexStart,
|
||||
align_items: FlexAlign::Stretch,
|
||||
align_content: FlexAlign::Stretch,
|
||||
width: Size::Stretch,
|
||||
height: Size::Auto,
|
||||
row_gap: 0,
|
||||
column_gap: 0,
|
||||
items: vec![FlexItem {
|
||||
node: label,
|
||||
order: 0,
|
||||
grow: 0.0,
|
||||
shrink: 0.0,
|
||||
basis: Size::Auto,
|
||||
align_self: FlexAlign::Stretch,
|
||||
}],
|
||||
};
|
||||
let mut outer = child_box(1, column, None);
|
||||
let LayoutNode::Box {
|
||||
width,
|
||||
content_width_exact,
|
||||
..
|
||||
} = &mut outer
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
*width = Size::Auto;
|
||||
*content_width_exact = true;
|
||||
let rendered = render_node_with_override(
|
||||
&outer,
|
||||
test_context(),
|
||||
false,
|
||||
Some(BoxOverride {
|
||||
content_width: Some(100),
|
||||
content_height: None,
|
||||
declared_width: None,
|
||||
}),
|
||||
)
|
||||
.expect("typed column render");
|
||||
assert_eq!(rendered.lines[0].width, 100);
|
||||
assert!(
|
||||
matches!(
|
||||
rendered.lines[0].atoms.first(),
|
||||
Some(Atom::Space { width: 45, .. })
|
||||
),
|
||||
"{:?}",
|
||||
rendered.lines[0].atoms
|
||||
);
|
||||
}
|
||||
|
||||
fn literal_from_full_tape(tape: &[u8]) -> &str {
|
||||
let literal_length = u64::from_le_bytes(tape[112..120].try_into().unwrap()) as usize;
|
||||
let line_count = u32::from_le_bytes(tape[120..124].try_into().unwrap()) as usize;
|
||||
@ -7151,7 +7384,7 @@ mod tests {
|
||||
assert_eq!(u64::from_le_bytes(encoded[92..100].try_into().unwrap()), 7);
|
||||
assert_eq!(u64::from_le_bytes(encoded[112..120].try_into().unwrap()), 6);
|
||||
let patch_count = u32::from_le_bytes(encoded[120..124].try_into().unwrap()) as usize;
|
||||
assert_eq!(patch_count, 3);
|
||||
assert_eq!(patch_count, 1);
|
||||
let reserved = u32::from_le_bytes(encoded[124..128].try_into().unwrap());
|
||||
let metadata_records = u64::from_le_bytes(encoded[128..136].try_into().unwrap());
|
||||
let payload_length = u64::from_le_bytes(encoded[136..144].try_into().unwrap()) as usize;
|
||||
@ -7168,9 +7401,9 @@ mod tests {
|
||||
assert!(fragment_length > 0);
|
||||
assert!(fragment_records > 0);
|
||||
assert_eq!(encoded.len(), payload_end + fragment_length);
|
||||
assert!(payload.starts_with("[#(\"Xe \""));
|
||||
assert!(payload.contains("1 2 (ebox-content 9 ebox-native-property-template-ids (4))"));
|
||||
assert!(payload.contains("2 3 (display (space :width (7)))"));
|
||||
assert!(payload.starts_with("[#(\"Xdef \""));
|
||||
assert!(payload.contains("2 3 (ebox-content 9 ebox-native-property-template-ids (4))"));
|
||||
assert!(payload.contains("4 5 (display (space :width (7)))"));
|
||||
assert!(payload.contains("(:prepared-p t"));
|
||||
assert!(payload.contains("(9 content) ((5 . 6))"));
|
||||
assert!(payload.contains("data (9 (5 . 6))"));
|
||||
|
||||
@ -286,7 +286,7 @@
|
||||
(let (cells)
|
||||
(dotimes (column 4)
|
||||
(push
|
||||
(apply #'ebox-create
|
||||
(apply #'ebox-test-box
|
||||
(append
|
||||
(list :key (list 'cell row column)
|
||||
:content (format "row-%d cell-%d\ndetail-%d-%d"
|
||||
@ -299,24 +299,23 @@
|
||||
'(:id target))
|
||||
(when styled '(:class card))))
|
||||
cells))
|
||||
(push (apply #'ebox-row (nreverse cells)) rows)))
|
||||
(apply #'ebox-column (nreverse rows))))
|
||||
(push (apply #'ebox-test-row (nreverse cells)) rows)))
|
||||
(apply #'ebox-test-column (nreverse rows))))
|
||||
|
||||
(defun ebox-performance-evaluator--resize-fixture ()
|
||||
"Return a viewport-dependent wrapped flex fixture."
|
||||
(let (items)
|
||||
(dotimes (index 12)
|
||||
(push
|
||||
(ebox-flex-item
|
||||
(ebox-create
|
||||
:key (list 'resize-item index)
|
||||
:content (format "resize-%02d" index)
|
||||
:width '(70)
|
||||
:padding '(0 1)
|
||||
:color "#172554"
|
||||
:bgcolor "#DBEAFE"))
|
||||
(ebox-test-box
|
||||
:key (list 'resize-item index)
|
||||
:content (format "resize-%02d" index)
|
||||
:width '(70)
|
||||
:padding '(0 1)
|
||||
:color "#172554"
|
||||
:bgcolor "#DBEAFE")
|
||||
items))
|
||||
(apply #'ebox-flex
|
||||
(apply #'ebox-test-flex
|
||||
:key 'resize-root
|
||||
:width '(viewport)
|
||||
:flex-wrap 'wrap
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
(defun ebox-visual-check-layout ()
|
||||
"Return the fixed layout used by visual verification."
|
||||
(let* ((title (ebox-create
|
||||
(let* ((title (ebox-test-box
|
||||
:content "Ebox Visual Check"
|
||||
:box-sizing 'border-box
|
||||
:width 74
|
||||
@ -37,7 +37,7 @@
|
||||
:border-color "#4A90D9"
|
||||
:color "#EAF6FF"
|
||||
:bgcolor "#203040"))
|
||||
(hidden (ebox-create
|
||||
(hidden (ebox-test-box
|
||||
:content "Hidden: visible line\nHIDDEN-CLIPPED-LINE"
|
||||
:box-sizing 'border-box
|
||||
:width 24
|
||||
@ -48,7 +48,7 @@
|
||||
:color "#F0FFF4"
|
||||
:bgcolor "#17351F"
|
||||
:overflow 'hidden))
|
||||
(scroll (ebox-create
|
||||
(scroll (ebox-test-box
|
||||
:content "Scroll: first\nSCROLL-SECOND\nSCROLL-THIRD"
|
||||
:box-sizing 'border-box
|
||||
:width 24
|
||||
@ -59,7 +59,7 @@
|
||||
:color "#FFF4E6"
|
||||
:bgcolor "#3A2410"
|
||||
:overflow 'scroll))
|
||||
(sized (ebox-create
|
||||
(sized (ebox-test-box
|
||||
:content "border-box max-height\nline 2\nSIZED-CLIPPED"
|
||||
:box-sizing 'border-box
|
||||
:width 26
|
||||
@ -70,7 +70,7 @@
|
||||
:color "#F8EEFF"
|
||||
:bgcolor "#2F1B3A"
|
||||
:overflow 'hidden))
|
||||
(footer (ebox-create
|
||||
(footer (ebox-test-box
|
||||
:content "Checks: text properties + display specs + GUI screenshot"
|
||||
:box-sizing 'border-box
|
||||
:width 74
|
||||
@ -79,7 +79,7 @@
|
||||
:border-color "#7F8C8D"
|
||||
:color "#F8F9F9"
|
||||
:bgcolor "#2C3E50")))
|
||||
(ebox-column title (ebox-row hidden scroll sized) footer)))
|
||||
(ebox-test-column title (ebox-test-row hidden scroll sized) footer)))
|
||||
|
||||
(defun ebox-visual-check--max-line-pixel-width (string)
|
||||
"Return maximum pixel width among STRING lines."
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
|
||||
(ert-deftest ebox-child-range-render-signature-snapshots-item-identities ()
|
||||
"A cached Range signature must not follow later runtime identity mutation."
|
||||
(let* ((item (ebox-create :content "item" :key 'item))
|
||||
(let* ((item (ebox-test-box :content "item" :key 'item))
|
||||
(sequence (ebox-child-range--build (list (cons 'range (list item)))))
|
||||
(published
|
||||
(aref (ebox-child-range--segment-payload
|
||||
@ -270,17 +270,17 @@
|
||||
|
||||
(ert-deftest ebox-child-range-integration-is-transparent-and-indexed ()
|
||||
"Compiled Range segments render as direct children and retain empty refs."
|
||||
(let* ((static (ebox-create :key 'a :class "a" :content "A" :width '(40)))
|
||||
(one (ebox-create :key 'b :class "b" :content "B" :width '(40)))
|
||||
(two (ebox-create :key 'c :class "c" :content "C" :width '(40)))
|
||||
(let* ((static (ebox-test-box :key 'a :class "a" :content "A" :width '(40)))
|
||||
(one (ebox-test-box :key 'b :class "b" :content "B" :width '(40)))
|
||||
(two (ebox-test-box :key 'c :class "c" :content "C" :width '(40)))
|
||||
(descriptor (ebox-child-range--descriptor-create 'items (list one two)))
|
||||
(empty (ebox-child-range--descriptor-create 'empty nil))
|
||||
(raw-items (ebox-child-range--descriptor-items descriptor))
|
||||
(ranged (ebox-column static empty descriptor))
|
||||
(direct (ebox-column
|
||||
(ebox-create :key 'a :class "a" :content "A" :width '(40))
|
||||
(ebox-create :key 'b :class "b" :content "B" :width '(40))
|
||||
(ebox-create :key 'c :class "c" :content "C" :width '(40))))
|
||||
(ranged (ebox-test-column static empty descriptor))
|
||||
(direct (ebox-test-column
|
||||
(ebox-test-box :key 'a :class "a" :content "A" :width '(40))
|
||||
(ebox-test-box :key 'b :class "b" :content "B" :width '(40))
|
||||
(ebox-test-box :key 'c :class "c" :content "C" :width '(40))))
|
||||
(left (generate-new-buffer " *ebox-range-direct*"))
|
||||
(right (generate-new-buffer " *ebox-range-compiled*")))
|
||||
(unwind-protect
|
||||
@ -335,32 +335,33 @@
|
||||
|
||||
(ert-deftest ebox-child-range-integration-rejects-invalid-placement-atomically ()
|
||||
"Range refs, keys, nesting, roots, and scalar slots fail before publication."
|
||||
(should-error
|
||||
(ebox-test-column
|
||||
(ebox-child-range--descriptor-create
|
||||
'outer (list (ebox-child-range--descriptor-create 'nested nil)))))
|
||||
(should-error
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'duplicate :content "a")
|
||||
(ebox-child-range--descriptor-create
|
||||
'range (list (ebox-test-box :key 'duplicate :content "b")))))
|
||||
(let ((buffer (generate-new-buffer " *ebox-range-invalid*")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-render-to-buffer buffer (ebox-create :key 'root :content "old"))
|
||||
(ebox-render-to-buffer buffer (ebox-test-box :key 'root :content "old"))
|
||||
(let ((before (with-current-buffer buffer (buffer-string)))
|
||||
(state (ebox--buffer-render-state buffer)))
|
||||
(dolist
|
||||
(root
|
||||
(list
|
||||
(ebox-child-range--descriptor-create
|
||||
'root (list (ebox-create :key 'x :content "x")))
|
||||
'root (list (ebox-test-box :key 'x :content "x")))
|
||||
(list :ebox-type 'box
|
||||
:ebox-content-node
|
||||
(ebox-child-range--descriptor-create 'scalar nil))
|
||||
(ebox-column
|
||||
(ebox-child-range--descriptor-create
|
||||
'outer
|
||||
(list (ebox-child-range--descriptor-create 'nested nil))))
|
||||
(ebox-column
|
||||
(ebox-test-column
|
||||
(ebox-child-range--descriptor-create 'same nil)
|
||||
(ebox-column
|
||||
(ebox-child-range--descriptor-create 'same nil)))
|
||||
(ebox-column
|
||||
(ebox-create :key 'duplicate :content "a")
|
||||
(ebox-child-range--descriptor-create
|
||||
'range (list (ebox-create :key 'duplicate :content "b"))))))
|
||||
(ebox-test-column
|
||||
(ebox-child-range--descriptor-create 'same nil)))))
|
||||
(should-error (ebox-commit buffer root))
|
||||
(should (eq (ebox--buffer-render-state buffer) state))
|
||||
(should (equal (with-current-buffer buffer (buffer-string))
|
||||
@ -383,24 +384,24 @@
|
||||
"Ranges add no ECSS subject and preserve child/sibling cascade semantics."
|
||||
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||
(direct-left
|
||||
(ebox-create :key 'left :host-ref 'left :class "left"
|
||||
(ebox-test-box :key 'left :host-ref 'left :class "left"
|
||||
:content "L" :width '(40)))
|
||||
(direct-right
|
||||
(ebox-create :key 'right :host-ref 'right :class "right"
|
||||
(ebox-test-box :key 'right :host-ref 'right :class "right"
|
||||
:content "R" :width '(40)))
|
||||
(range-left
|
||||
(ebox-create :key 'left :host-ref 'left :class "left"
|
||||
(ebox-test-box :key 'left :host-ref 'left :class "left"
|
||||
:content "L" :width '(40)))
|
||||
(range-right
|
||||
(ebox-create :key 'right :host-ref 'right :class "right"
|
||||
(ebox-test-box :key 'right :host-ref 'right :class "right"
|
||||
:content "R" :width '(40)))
|
||||
(left-range
|
||||
(ebox-child-range--descriptor-create 'left-range (list range-left)))
|
||||
(right-range
|
||||
(ebox-child-range--descriptor-create 'right-range (list range-right)))
|
||||
(direct (plist-put (ebox-column direct-left direct-right)
|
||||
(direct (plist-put (ebox-test-column direct-left direct-right)
|
||||
:class "parent"))
|
||||
(ranged (plist-put (ebox-column left-range right-range)
|
||||
(ranged (plist-put (ebox-test-column left-range right-range)
|
||||
:class "parent"))
|
||||
(direct-buffer (generate-new-buffer " *ebox-range-ecss-direct*"))
|
||||
(range-buffer (generate-new-buffer " *ebox-range-ecss-range*"))
|
||||
@ -450,13 +451,13 @@
|
||||
(ert-deftest ebox-child-range-path-copy-replaces-compiled-material-child ()
|
||||
"Tree path copying cannot ignore a child stored in a compiled sequence."
|
||||
(let* ((source
|
||||
(ebox-column
|
||||
(ebox-create :key 'static :content "static")
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'static :content "static")
|
||||
(ebox-child-range--descriptor-create
|
||||
'items (list (ebox-create :key 'old :content "old")))))
|
||||
'items (list (ebox-test-box :key 'old :content "old")))))
|
||||
(compiled (ebox-tree-copy-node-structure source))
|
||||
(old-child (cadr (ebox-tree-layout-children compiled)))
|
||||
(new-child (ebox-create :key 'new :content "new"))
|
||||
(new-child (ebox-test-box :key 'new :content "new"))
|
||||
(unchanged (ebox-tree-copy-with-direct-child-replacements compiled nil))
|
||||
(copy (ebox-tree-copy-with-direct-child-replacements
|
||||
compiled (list (cons old-child new-child)))))
|
||||
@ -467,20 +468,49 @@
|
||||
'(static new)))
|
||||
(should (equal (mapcar (lambda (node) (plist-get node :key))
|
||||
(ebox-tree-layout-children compiled))
|
||||
'(static old)))))
|
||||
'(static old)))
|
||||
(let* ((original (ebox-text-create :value "old"))
|
||||
(range (ebox-child-range 'typed original))
|
||||
(typed (ebox-box-create
|
||||
:layout (ebox-column-layout-create)
|
||||
:children (list range)))
|
||||
(owned (car (ebox-box-node-children typed)))
|
||||
(replacement (ebox-text-create :value "new"))
|
||||
(replaced
|
||||
(ebox-tree-copy-with-direct-child-replacements
|
||||
typed (list (cons owned replacement))))
|
||||
direct)
|
||||
(ebox-tree-for-each-direct-child
|
||||
replaced (lambda (child) (push child direct)))
|
||||
(setq direct (nreverse direct))
|
||||
(dolist (view (list (ebox-box-node-children replaced)
|
||||
direct
|
||||
(ebox-tree-layout-children replaced)
|
||||
(ebox-tree-semantic-children replaced)))
|
||||
(should (= (length view) 1))
|
||||
(should (eq (car view) replacement)))
|
||||
(should (eq (ebox-tree-validate-declarative-root replaced) replaced))
|
||||
(let ((removed
|
||||
(ebox-tree-copy-with-direct-child-replacements
|
||||
typed (list (cons owned nil)))))
|
||||
(should-not (ebox-box-node-children removed))
|
||||
(should-not (ebox-tree-layout-children removed))
|
||||
(should-not (ebox-tree-semantic-children removed))
|
||||
(should (eq (ebox-tree-validate-declarative-root removed)
|
||||
removed))))))
|
||||
|
||||
(ert-deftest ebox-child-range-empty-sequence-is-authoritative ()
|
||||
"An all-empty sequence cannot fall through to legacy alias children."
|
||||
(let* ((sequence (ebox-child-range--build (list (cons 'empty nil))))
|
||||
(legacy (ebox-create :key 'legacy :content "legacy"))
|
||||
(legacy (ebox-test-box :key 'legacy :content "legacy"))
|
||||
(node (list :ebox-type 'stack :ebox-child-sequence sequence
|
||||
:top legacy :bottom legacy)))
|
||||
(should-not (ebox-tree-layout-children node))))
|
||||
|
||||
(ert-deftest ebox-child-range-validator-covers-wrapper-and-scalar-slots ()
|
||||
"Flat Range compilation cannot hide wrappers or admit scalar descriptors."
|
||||
(let* ((wrapper (ebox-create :key 'wrapper :host-ref 'wrapper :content "W"))
|
||||
(item (ebox-create :key 'item :host-ref 'item :content "I"))
|
||||
(let* ((wrapper (ebox-test-box :key 'wrapper :host-ref 'wrapper :content "W"))
|
||||
(item (ebox-test-box :key 'item :host-ref 'item :content "I"))
|
||||
(descriptor (ebox-child-range--descriptor-create 'items (list item)))
|
||||
(source (list :ebox-type 'flex :display '(block flex)
|
||||
:box wrapper :children (list descriptor))))
|
||||
@ -498,23 +528,23 @@
|
||||
(ebox-tree-validate-declarative-root
|
||||
(list :ebox-type 'stack :display '(block column)
|
||||
:top (ebox-child-range--descriptor-create 'bad nil)
|
||||
:bottom (ebox-create :key 'bottom :content "B"))))
|
||||
:bottom (ebox-test-box :key 'bottom :content "B"))))
|
||||
(should-error
|
||||
(ebox-tree-validate-declarative-root
|
||||
(list :ebox-type 'flex :display '(block flex)
|
||||
:box (ebox-create :key 'same :host-ref 'same :content "W")
|
||||
:box (ebox-test-box :key 'same :host-ref 'same :content "W")
|
||||
:children
|
||||
(list (ebox-child-range--descriptor-create
|
||||
'items
|
||||
(list (ebox-create :key 'item :host-ref 'same
|
||||
(list (ebox-test-box :key 'item :host-ref 'same
|
||||
:content "I")))))))))
|
||||
|
||||
(ert-deftest ebox-child-range-public-candidate-splices-and-reports ()
|
||||
"Public child Ranges support empty and repeated last-wins candidate splices."
|
||||
(let* ((buffer (generate-new-buffer " *ebox-public-range*"))
|
||||
(source
|
||||
(ebox-column
|
||||
(ebox-create :key 'static :content "S")
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'static :content "S")
|
||||
(ebox-child-range 'items)))
|
||||
report)
|
||||
(unwind-protect
|
||||
@ -522,10 +552,10 @@
|
||||
(ebox-render-to-buffer buffer source)
|
||||
(dolist (items
|
||||
(list
|
||||
(list (ebox-create :key 'one :content "1"))
|
||||
(list (ebox-create :key 'two :content "2")
|
||||
(ebox-create :key 'three :content "3"))
|
||||
(list (ebox-create :key 'final :content "F"))
|
||||
(list (ebox-test-box :key 'one :content "1"))
|
||||
(list (ebox-test-box :key 'two :content "2")
|
||||
(ebox-test-box :key 'three :content "3"))
|
||||
(list (ebox-test-box :key 'final :content "F"))
|
||||
nil))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref candidate 'items items)
|
||||
@ -545,23 +575,23 @@
|
||||
((rows (selected)
|
||||
(cl-loop for id from 1 to 12
|
||||
collect
|
||||
(ebox-create
|
||||
(ebox-test-box
|
||||
:key id :content (format "row-%02d" id) :width '(420)
|
||||
:color (and (= id selected) "#2F6B43"))))
|
||||
(root ()
|
||||
(ebox-create
|
||||
(ebox-test-box
|
||||
:width '(900) :color "#172033" :wrap-mode nil
|
||||
:ebox-content-node
|
||||
(ebox-flex
|
||||
(ebox-test-flex
|
||||
:width '(900)
|
||||
(ebox-create
|
||||
(ebox-test-box
|
||||
: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
|
||||
(ebox-test-column
|
||||
(apply #'ebox-child-range 'items (rows 1))))
|
||||
(ebox-create :key 'detail :host-ref 'detail
|
||||
(ebox-test-box :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)
|
||||
@ -576,7 +606,7 @@
|
||||
candidate 'items (rows selected))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'detail
|
||||
(ebox-create :key 'detail :host-ref 'detail
|
||||
(ebox-test-box :key 'detail :host-ref 'detail
|
||||
:width 'stretch :min-width 0
|
||||
:min-height 24 :padding '(2 2)
|
||||
:border "#687386"
|
||||
@ -592,7 +622,8 @@
|
||||
(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 :tp-scope-count) 0))
|
||||
(should-not (plist-get report :tp-full-root))
|
||||
(should (= (plist-get report :created-objects) 0))
|
||||
(should (= (plist-get report :removed-objects) 0))))))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer))))))
|
||||
@ -603,7 +634,7 @@
|
||||
(cl-labels
|
||||
((rows (keys)
|
||||
(mapcar (lambda (key)
|
||||
(ebox-create :key key
|
||||
(ebox-test-box :key key
|
||||
:content (format "row-%s" key)))
|
||||
keys))
|
||||
(row-id (key)
|
||||
@ -624,7 +655,7 @@
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column
|
||||
(ebox-test-column
|
||||
(apply #'ebox-child-range 'items (rows '(1 2 3 4)))))
|
||||
(let ((initial-1 (row-id 1))
|
||||
(initial-4 (row-id 4)))
|
||||
@ -643,24 +674,180 @@
|
||||
|
||||
(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))
|
||||
(dolist (items (list nil (list (ebox-create :key 'one :content "1"))))
|
||||
(dolist (case '((ebox-test-row row) (ebox-test-column column)))
|
||||
(dolist (items (list nil (list (ebox-test-box :key 'one :content "1"))))
|
||||
(let* ((range (apply #'ebox-child-range 'items items))
|
||||
(parent (funcall constructor range)))
|
||||
(parent (funcall (car case) range)))
|
||||
(should-not (ebox-child-range--descriptor-p parent))
|
||||
(should (eq (plist-get parent :ebox-type)
|
||||
(if (eq constructor 'ebox-row) 'concat 'stack)))
|
||||
(should (equal (plist-get parent :children) (list range)))
|
||||
(should (ebox-box-node-p parent))
|
||||
(should (eq (ebox-layout-config-kind (ebox-box-node-layout parent))
|
||||
(cadr case)))
|
||||
(should (equal (ebox-box-node-children parent) items))
|
||||
(should (equal (ebox-box-node-range-anchors parent)
|
||||
(list (list :ref 'items :before 0
|
||||
:after (length items)))))
|
||||
(should (stringp (ebox-render parent)))))))
|
||||
|
||||
(ert-deftest ebox-child-range-dsl-layout-child-keeps-props-wrapper ()
|
||||
"Pass a Range through row/column DSL children while retaining properties."
|
||||
(dolist (tag '(row column))
|
||||
(let* ((range (ebox-child-range 'items))
|
||||
(node (ebox-build (list tag :color "red" range))))
|
||||
(should (equal "red" (plist-get node :color)))
|
||||
(should (not (ebox-child-range--descriptor-p node)))
|
||||
(should-error (ebox-build range)))))
|
||||
(ert-deftest ebox-child-range-typed-box-preserves-transparent-range ()
|
||||
"Typed Box should retain Range identity outside its visual child model."
|
||||
(let* ((before (ebox-text-create :value "before" :key 'before))
|
||||
(first (ebox-text-create :value "A" :key 'first))
|
||||
(second (ebox-text-create :value "B" :key 'second))
|
||||
(after (ebox-text-create :value "after" :key 'after))
|
||||
(range (ebox-child-range 'items first second))
|
||||
(range-items (ebox-child-range--descriptor-items range))
|
||||
(box (ebox-box-create
|
||||
:layout (ebox-column-layout-create)
|
||||
:children (list before range after)))
|
||||
(buffer (generate-new-buffer " *ebox-typed-range*")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(should (equal (ebox-box-node-children box)
|
||||
(list before first second after)))
|
||||
(cl-mapc (lambda (actual expected)
|
||||
(should (eq actual expected)))
|
||||
(ebox-box-node-children box)
|
||||
(list before (car range-items) (cadr range-items) after))
|
||||
(should (equal (ebox-box-node-range-anchors box)
|
||||
'((:ref items :before 1 :after 3))))
|
||||
(let* ((empty (ebox-child-range 'empty))
|
||||
(empty-box
|
||||
(ebox-box-create
|
||||
:layout (ebox-column-layout-create)
|
||||
:children (list before empty after))))
|
||||
(should (equal (ebox-box-node-children empty-box)
|
||||
(list before after)))
|
||||
(should (equal (ebox-box-node-range-anchors empty-box)
|
||||
'((:ref empty :before 1 :after 1)))))
|
||||
(should
|
||||
(equal
|
||||
(mapcar (lambda (line)
|
||||
(string-trim-right (substring-no-properties line)))
|
||||
(ebox-string-lines (ebox-render box)))
|
||||
'("before" "A" "B" "after")))
|
||||
(ebox-render-to-buffer buffer box)
|
||||
(let* ((runtime-root (ebox--buffer-root-node buffer))
|
||||
(snapshot
|
||||
(ebox-tree-runtime-identity-snapshot runtime-root)))
|
||||
(should (= (plist-get snapshot :node-count) 5))
|
||||
(should (= (length (plist-get
|
||||
(plist-get snapshot :root) :children))
|
||||
4)))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-text-create :value "C" :key 'replacement)))
|
||||
(ebox-commit buffer candidate))
|
||||
(should
|
||||
(equal
|
||||
(with-current-buffer buffer
|
||||
(mapcar (lambda (line)
|
||||
(string-trim-right
|
||||
(substring-no-properties line)))
|
||||
(ebox-string-lines (buffer-string))))
|
||||
'("before" "C" "after")))
|
||||
(let ((before-style-state (ebox--buffer-render-state buffer))
|
||||
(before-style-text
|
||||
(with-current-buffer buffer (buffer-string)))
|
||||
(candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-test-build '(text :color "red" "B"))))
|
||||
(cl-letf (((symbol-function 'accept-change-group)
|
||||
(lambda (_group) (error "reject styled Range"))))
|
||||
(should-error (ebox-commit buffer candidate) :type 'error))
|
||||
(should (eq before-style-state
|
||||
(ebox--buffer-render-state buffer)))
|
||||
(should (equal before-style-text
|
||||
(with-current-buffer buffer (buffer-string)))))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-test-build '(text :color "red" "B"))))
|
||||
(ebox-commit buffer candidate))
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(root (plist-get state :root-node))
|
||||
(rendered (with-current-buffer buffer (buffer-string)))
|
||||
(position (let ((case-fold-search nil))
|
||||
(string-match "B" rendered)))
|
||||
(face (and position
|
||||
(get-text-property position 'face rendered))))
|
||||
(should (zerop (ebox-tree-author-style-count root)))
|
||||
(should-not (plist-get state :cascade-required-p))
|
||||
(should
|
||||
(or (and (listp face)
|
||||
(keywordp (car face))
|
||||
(equal (plist-get face :foreground) "red"))
|
||||
(cl-some (lambda (entry)
|
||||
(and (listp entry)
|
||||
(equal (plist-get entry :foreground) "red")))
|
||||
face))))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-text-create :value "D" :key 'unstyled)))
|
||||
(ebox-commit buffer candidate))
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(root (plist-get state :root-node))
|
||||
(rendered (with-current-buffer buffer (buffer-string)))
|
||||
(position (string-match "D" rendered))
|
||||
(face (and position
|
||||
(get-text-property position 'face rendered))))
|
||||
(should (zerop (ebox-tree-author-style-count root)))
|
||||
(should-not
|
||||
(or (and (listp face)
|
||||
(keywordp (car face))
|
||||
(equal (plist-get face :foreground) "red"))
|
||||
(cl-some (lambda (entry)
|
||||
(and (listp entry)
|
||||
(equal (plist-get entry :foreground) "red")))
|
||||
face))))
|
||||
(let ((before-invalid
|
||||
(with-current-buffer buffer (buffer-string)))
|
||||
(candidate (ebox-candidate-begin buffer)))
|
||||
(should-error
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (list :ebox-type 'box :content "legacy")))
|
||||
:type 'error)
|
||||
(should (equal before-invalid
|
||||
(with-current-buffer buffer (buffer-string))))))
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-child-range-typed-render-reuses-material-child-list ()
|
||||
"Typed Range rendering should not repeatedly flatten its child sequence."
|
||||
(let* ((items
|
||||
(cl-loop for index below 100
|
||||
collect (ebox-text-create
|
||||
:value (number-to-string index) :key index)))
|
||||
(range (apply #'ebox-child-range 'items items))
|
||||
(box (ebox-box-create
|
||||
:layout (ebox-column-layout-create)
|
||||
:children (list range)))
|
||||
(original (symbol-function 'ebox-child-range--flatten))
|
||||
(calls 0))
|
||||
(cl-letf (((symbol-function 'ebox-child-range--flatten)
|
||||
(lambda (sequence)
|
||||
(cl-incf calls)
|
||||
(funcall original sequence))))
|
||||
(ebox-render box))
|
||||
(should (zerop calls))
|
||||
(let* ((replacement (ebox-text-create :value "replacement" :key 'new))
|
||||
(updated
|
||||
(car (ebox-child-range--replace
|
||||
(plist-get box :ebox-child-sequence)
|
||||
'items (list replacement))))
|
||||
(candidate (copy-sequence box)))
|
||||
(ebox-tree--put-child-sequence candidate updated)
|
||||
(setq calls 0)
|
||||
(cl-letf (((symbol-function 'ebox-child-range--flatten)
|
||||
(lambda (sequence)
|
||||
(cl-incf calls)
|
||||
(funcall original sequence))))
|
||||
(ebox-tree-subject-index candidate)
|
||||
(ebox-tree-layout-children candidate))
|
||||
(should (= calls 1)))))
|
||||
|
||||
(ert-deftest ebox-child-range-empty-owner-falls-back-atomically ()
|
||||
"Publish empty sole Range through full surface fallback and rollback safely."
|
||||
@ -668,17 +855,17 @@
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-render-to-buffer buffer
|
||||
(ebox-column (ebox-child-range 'items)))
|
||||
(ebox-test-column (ebox-child-range 'items)))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items (list (ebox-create :key 'one :content "1")))
|
||||
candidate 'items (list (ebox-test-box :key 'one :content "1")))
|
||||
(cl-letf (((symbol-function 'accept-change-group)
|
||||
(lambda (&rest _) (error "injected empty accept"))))
|
||||
(should-error (ebox-commit buffer candidate) :type 'error)))
|
||||
(should (equal "" (with-current-buffer buffer (buffer-string))))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items (list (ebox-create :key 'one :content "1")))
|
||||
candidate 'items (list (ebox-test-box :key 'one :content "1")))
|
||||
(let ((report (ebox-commit buffer candidate)))
|
||||
(should (plist-get report :empty-range-owner-fallback))))
|
||||
(should (string-match-p "1" (with-current-buffer buffer
|
||||
@ -694,20 +881,20 @@
|
||||
(let* ((buffer (generate-new-buffer " *ebox-range-no-flatten*"))
|
||||
(static
|
||||
(cl-loop for index below 100
|
||||
collect (ebox-create :key (list 'static index)
|
||||
collect (ebox-test-box :key (list 'static index)
|
||||
:content (number-to-string index))))
|
||||
(root (apply #'ebox-column
|
||||
(root (apply #'ebox-test-column
|
||||
(append static
|
||||
(list (ebox-child-range
|
||||
'items
|
||||
(ebox-create :key 'old :content "old"))))))
|
||||
(ebox-test-box :key 'old :content "old"))))))
|
||||
candidate logical trace deltas)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-render-to-buffer buffer root)
|
||||
(setq candidate (ebox-candidate-begin buffer))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items (list (ebox-create :key 'new :content "new")))
|
||||
candidate 'items (list (ebox-test-box :key 'new :content "new")))
|
||||
(setq trace (make-hash-table :test 'equal))
|
||||
(cl-letf (((symbol-function 'ebox-child-range--flatten)
|
||||
(lambda (&rest _) (error "candidate flattened Range"))))
|
||||
@ -730,15 +917,15 @@
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column
|
||||
(ebox-child-range 'left (ebox-create :key 'move :content "L"))
|
||||
(ebox-child-range 'right (ebox-create :key 'stay :content "R"))))
|
||||
(ebox-test-column
|
||||
(ebox-child-range 'left (ebox-test-box :key 'move :content "L"))
|
||||
(ebox-child-range 'right (ebox-test-box :key 'stay :content "R"))))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref candidate 'left nil)
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'right
|
||||
(list (ebox-create :key 'move :content "M")
|
||||
(ebox-create :key 'stay :content "R")))
|
||||
(list (ebox-test-box :key 'move :content "M")
|
||||
(ebox-test-box :key 'stay :content "R")))
|
||||
(ebox-commit buffer candidate))
|
||||
(should (string-match-p "M" (with-current-buffer buffer
|
||||
(buffer-string)))))
|
||||
@ -749,13 +936,13 @@
|
||||
(dolist (static-count '(10 100 500))
|
||||
(let* ((buffer (generate-new-buffer " *ebox-range-public-gate*"))
|
||||
(root
|
||||
(apply #'ebox-column
|
||||
(apply #'ebox-test-column
|
||||
(append
|
||||
(cl-loop for index below static-count
|
||||
collect (ebox-create :key (list 'static index)
|
||||
collect (ebox-test-box :key (list 'static index)
|
||||
:content "s"))
|
||||
(list (ebox-child-range
|
||||
'items (ebox-create :key 'old :content "o")))))))
|
||||
'items (ebox-test-box :key 'old :content "o")))))))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-render-to-buffer buffer root)
|
||||
@ -768,8 +955,8 @@
|
||||
report)
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-create :key 'new-a :content "a")
|
||||
(ebox-create :key 'new-b :content "b")))
|
||||
(list (ebox-test-box :key 'new-a :content "a")
|
||||
(ebox-test-box :key 'new-b :content "b")))
|
||||
(cl-letf (((symbol-function 'ebox--runtime-index)
|
||||
(lambda (&rest _) (error "unexpected full index"))))
|
||||
(setq report (ebox-commit buffer candidate)))
|
||||
@ -814,13 +1001,13 @@
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column (ebox-create :key 'a :host-ref 'a :content "a")
|
||||
(ebox-create :key 'b :content "b")))
|
||||
(ebox-test-column (ebox-test-box :key 'a :host-ref 'a :content "a")
|
||||
(ebox-test-box :key 'b :content "b")))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'a
|
||||
(ebox-column
|
||||
(ebox-create :key 'a :host-ref 'a :content "a")
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'a :host-ref 'a :content "a")
|
||||
(ebox-child-range 'introduced)))
|
||||
(should-error
|
||||
(ebox-candidate-replace-range-ref candidate 'introduced nil))
|
||||
@ -828,7 +1015,7 @@
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'introduced
|
||||
(list (ebox-create :key 'later :content "later")))
|
||||
(list (ebox-test-box :key 'later :content "later")))
|
||||
(ebox-commit buffer candidate))
|
||||
(should (string-match-p "later" (with-current-buffer buffer
|
||||
(buffer-string)))))
|
||||
@ -842,15 +1029,15 @@
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column
|
||||
(ebox-test-column
|
||||
(plist-put
|
||||
(ebox-column
|
||||
(ebox-create :key 'static :content "s")
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'static :content "s")
|
||||
(ebox-child-range
|
||||
'items (ebox-create :key 'child :host-ref 'child
|
||||
'items (ebox-test-box :key 'child :host-ref 'child
|
||||
:content "old")))
|
||||
:host-ref 'parent)
|
||||
(ebox-create :key 'peer :content "peer")))
|
||||
(ebox-test-box :key 'peer :content "peer")))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(dolist (operation (if (eq order 'host-first)
|
||||
'(host range) '(range host)))
|
||||
@ -858,11 +1045,11 @@
|
||||
('host
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'parent
|
||||
(ebox-create :key 'parent-new :content "HOST-WINS")))
|
||||
(ebox-test-box :key 'parent-new :content "HOST-WINS")))
|
||||
('range
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-create :key 'range-new
|
||||
(list (ebox-test-box :key 'range-new
|
||||
:content "range-loses"))))))
|
||||
(ebox-commit buffer candidate))
|
||||
(should (string-match-p "HOST-WINS"
|
||||
@ -877,15 +1064,15 @@
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column
|
||||
(ebox-create :key 'static :content "s")
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'static :content "s")
|
||||
(ebox-child-range
|
||||
'items (ebox-create :key 'child :host-ref 'child :content "0"))))
|
||||
'items (ebox-test-box :key 'child :host-ref 'child :content "0"))))
|
||||
(dotimes (turn 2)
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'child
|
||||
(ebox-create :key 'child :host-ref 'child
|
||||
(ebox-test-box :key 'child :host-ref 'child
|
||||
:content (number-to-string (1+ turn))))
|
||||
(ebox-commit buffer candidate))
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
@ -905,31 +1092,31 @@
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column
|
||||
(ebox-test-column
|
||||
(plist-put
|
||||
(ebox-column
|
||||
(ebox-create :key 'static :content "s")
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'static :content "s")
|
||||
(ebox-child-range
|
||||
'items (ebox-create :key 'child :host-ref 'child
|
||||
'items (ebox-test-box :key 'child :host-ref 'child
|
||||
:content "old")))
|
||||
:host-ref 'parent)
|
||||
(ebox-create :key 'peer :content "peer")))
|
||||
(ebox-test-box :key 'peer :content "peer")))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'parent
|
||||
(plist-put
|
||||
(ebox-column
|
||||
(ebox-create :key 'static :content "s2")
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'static :content "s2")
|
||||
(ebox-child-range
|
||||
'items (ebox-create :key 'child :host-ref 'child
|
||||
'items (ebox-test-box :key 'child :host-ref 'child
|
||||
:content "ancestor")))
|
||||
:host-ref 'parent))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-create :key 'range :content "RANGE-LOSES")))
|
||||
(list (ebox-test-box :key 'range :content "RANGE-LOSES")))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'child
|
||||
(ebox-create :key 'child :host-ref 'child
|
||||
(ebox-test-box :key 'child :host-ref 'child
|
||||
:content "DESCENDANT-WINS"))
|
||||
(ebox-commit buffer candidate))
|
||||
(let ((text (with-current-buffer buffer (buffer-string))))
|
||||
@ -941,10 +1128,10 @@
|
||||
"M=64 remains an affected-payload local delta under 500 static segments."
|
||||
(let* ((buffer (generate-new-buffer " *ebox-range-large-payload*"))
|
||||
(root
|
||||
(apply #'ebox-column
|
||||
(apply #'ebox-test-column
|
||||
(append
|
||||
(cl-loop for index below 500
|
||||
collect (ebox-create :key (list 'static index)
|
||||
collect (ebox-test-box :key (list 'static index)
|
||||
:content "s"))
|
||||
(list (ebox-child-range 'items)))))
|
||||
report)
|
||||
@ -955,7 +1142,7 @@
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(cl-loop for index below 64
|
||||
collect (ebox-create :key (list 'new index)
|
||||
collect (ebox-test-box :key (list 'new index)
|
||||
:content "n")))
|
||||
(cl-letf (((symbol-function
|
||||
'ebox-incremental--candidate-full-index-preparation)
|
||||
@ -977,10 +1164,10 @@
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column
|
||||
(ebox-create :key 'static :content "s")
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'static :content "s")
|
||||
(ebox-child-range
|
||||
'items (ebox-create :key 'child :host-ref 'child
|
||||
'items (ebox-test-box :key 'child :host-ref 'child
|
||||
:content "old"))))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(dolist (operation (if (eq order 'host-first)
|
||||
@ -988,11 +1175,11 @@
|
||||
(if (eq operation 'host)
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'child
|
||||
(ebox-create :key 'child :host-ref 'child
|
||||
(ebox-test-box :key 'child :host-ref 'child
|
||||
:content "HOST-LOSES"))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-create :key 'range :content "RANGE-WINS")))))
|
||||
(list (ebox-test-box :key 'range :content "RANGE-WINS")))))
|
||||
(ebox-commit buffer candidate))
|
||||
(let ((text (with-current-buffer buffer (buffer-string))))
|
||||
(should (string-match-p "RANGE-WINS" text))
|
||||
@ -1006,29 +1193,29 @@
|
||||
(cl-labels
|
||||
((child (static-text range-ref range-key range-text)
|
||||
(let ((node
|
||||
(ebox-column
|
||||
(ebox-create :key 'child-static :content static-text)
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'child-static :content static-text)
|
||||
(if range-key
|
||||
(ebox-child-range
|
||||
range-ref
|
||||
(ebox-create :key range-key :content range-text))
|
||||
(ebox-test-box :key range-key :content range-text))
|
||||
(ebox-child-range range-ref)))))
|
||||
(plist-put node :key 'child)
|
||||
(plist-put node :host-ref 'child)
|
||||
node))
|
||||
(ancestor (child-node peer-text)
|
||||
(let ((node
|
||||
(ebox-column
|
||||
(ebox-test-column
|
||||
child-node
|
||||
(ebox-create :key 'ancestor-peer :content peer-text))))
|
||||
(ebox-test-box :key 'ancestor-peer :content peer-text))))
|
||||
(plist-put node :key 'ancestor)
|
||||
(plist-put node :host-ref 'ancestor)
|
||||
node)))
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column
|
||||
(ebox-test-column
|
||||
(ancestor (child "s" 'old-ref nil nil) "p")
|
||||
(ebox-create :key 'root-peer :content "r")))
|
||||
(ebox-test-box :key 'root-peer :content "r")))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'ancestor
|
||||
@ -1054,20 +1241,20 @@
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column
|
||||
(ebox-create :key 'host :host-ref 'host :content "old-host")
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'host :host-ref 'host :content "old-host")
|
||||
(ebox-child-range
|
||||
'items (ebox-create :key 'old-range :content "old-range"))))
|
||||
'items (ebox-test-box :key 'old-range :content "old-range"))))
|
||||
(let* ((surface (with-current-buffer buffer ebox-surface--buffer-surface))
|
||||
(revision (tp-surface-revision surface))
|
||||
(candidate (ebox-candidate-begin buffer))
|
||||
report)
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'host
|
||||
(ebox-create :key 'host :host-ref 'host :content "new-host"))
|
||||
(ebox-test-box :key 'host :host-ref 'host :content "new-host"))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-create :key 'new-range :content "new-range")))
|
||||
(list (ebox-test-box :key 'new-range :content "new-range")))
|
||||
(setq report
|
||||
(ebox-commit buffer candidate
|
||||
(lambda (_report) (cl-incf callbacks))))
|
||||
@ -1088,10 +1275,10 @@
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column
|
||||
(ebox-create :key 'static :content "s")
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'static :content "s")
|
||||
(ebox-child-range
|
||||
'items (ebox-create :key 'old :host-ref 'old :content "old"))))
|
||||
'items (ebox-test-box :key 'old :host-ref 'old :content "old"))))
|
||||
(let* ((old-state (ebox--buffer-render-state buffer))
|
||||
(old-root (plist-get old-state :root-node))
|
||||
(old-table (plist-get old-state :range-ref-table))
|
||||
@ -1100,7 +1287,7 @@
|
||||
(candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-create :key 'new :host-ref 'new :content "new")))
|
||||
(list (ebox-test-box :key 'new :host-ref 'new :content "new")))
|
||||
(cl-letf (((symbol-function 'accept-change-group)
|
||||
(lambda (_group) (error "reject final accept"))))
|
||||
(should-error
|
||||
@ -1119,7 +1306,7 @@
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-create :key 'new :content "new")))
|
||||
(list (ebox-test-box :key 'new :content "new")))
|
||||
(ebox-commit buffer candidate)))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
@ -1130,10 +1317,10 @@
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-column
|
||||
(ebox-create :key 'static :content "s")
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'static :content "s")
|
||||
(ebox-child-range
|
||||
'items (ebox-create :key 'old :content "old"))))
|
||||
'items (ebox-test-box :key 'old :content "old"))))
|
||||
(let* ((old-state (ebox--buffer-render-state buffer))
|
||||
(old-root (plist-get old-state :root-node))
|
||||
(old-range-table (plist-get old-state :range-ref-table))
|
||||
@ -1142,7 +1329,7 @@
|
||||
trace)
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-create :key 'new :content "new")))
|
||||
(list (ebox-test-box :key 'new :content "new")))
|
||||
(should-error
|
||||
(ebox-commit
|
||||
buffer candidate
|
||||
@ -1157,7 +1344,7 @@
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-range-ref
|
||||
candidate 'items
|
||||
(list (ebox-create :key 'new :content "new")))
|
||||
(list (ebox-test-box :key 'new :content "new")))
|
||||
(ebox-commit buffer candidate))
|
||||
(should (string-match-p "new" (with-current-buffer buffer
|
||||
(buffer-string)))))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -149,7 +149,8 @@
|
||||
(should-not (ebox-box-node-p node))
|
||||
(should (eq (ebox-node-source-handle node) 'source-text))
|
||||
(should (string= (ebox-text-node-value node) "Hello"))
|
||||
(should (string= (ebox-dsl-test--plain node) "Hello"))))
|
||||
(should (string= (ebox-dsl-test--plain node) "Hello"))
|
||||
(should-not (ebox-tree-node-visible-overflow-p node))))
|
||||
|
||||
(ert-deftest ebox-canonical-text-constructor-rejects-non-text-shapes ()
|
||||
"A canonical TextNode should have exactly one string payload."
|
||||
@ -157,6 +158,7 @@
|
||||
(should-error (ebox-text-create :value "A" :content "B") :type 'error)
|
||||
(should-error (ebox-text-create :value "A" :children nil) :type 'error)
|
||||
(should-error (ebox-text-create :value "A" :color "red") :type 'error)
|
||||
(should-error (ebox-build '(text :wrap-mode none "A")) :type 'error)
|
||||
(should-error
|
||||
(ebox-text-create :value "A" :value "B")
|
||||
:type 'error)
|
||||
@ -164,6 +166,89 @@
|
||||
(ebox-text-create :value "A" :source-handle 'a :source-handle 'b)
|
||||
:type 'error))
|
||||
|
||||
(ert-deftest ebox-canonical-box-uses-registered-runtime-defaults ()
|
||||
"A Box should materialize the definite defaults consumed by layout."
|
||||
(let ((box (ebox-box-create
|
||||
:layout (ebox-normal-layout-create) :children nil)))
|
||||
(should (eq (ebox-get box :overflow) 'scroll))
|
||||
(dolist (property '(: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))
|
||||
(should (zerop (ebox-get box property))))))
|
||||
|
||||
(ert-deftest ebox-render-cache-signature-ignores-derived-layout-proofs ()
|
||||
"A render must not invalidate its own canonical node cache key."
|
||||
(let* ((node (ebox-build '(box :width '(80) "Stable")))
|
||||
(before
|
||||
(let ((ebox--render-cache-signature-cache
|
||||
(make-hash-table :test 'eq)))
|
||||
(ebox--render-cache-node-signature node))))
|
||||
(plist-put node :ebox-content-width-exact-p t)
|
||||
(plist-put node :ebox-content-layout-complete-p t)
|
||||
(let ((after
|
||||
(let ((ebox--render-cache-signature-cache
|
||||
(make-hash-table :test 'eq)))
|
||||
(ebox--render-cache-node-signature node))))
|
||||
(should (equal-including-properties before after)))))
|
||||
|
||||
(ert-deftest ebox-canonical-box-materializes-inherited-foreground-once ()
|
||||
"A canonical Text should be the sole foreground paint owner under Box."
|
||||
(let* ((rendered (ebox-render (ebox-build '(box :color "#123456" "A"))))
|
||||
(face (get-text-property 0 'face rendered)))
|
||||
(should (equal face '(:foreground "#123456")))))
|
||||
|
||||
(ert-deftest ebox-box-owns-wrap-policy-consumed-by-text ()
|
||||
"Box declarations should become inherited Text measurement input."
|
||||
(let ((buffer (generate-new-buffer " *ebox-wrap-owner*")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer (ebox-build '(box :wrap-mode none "Page 1 / 3")))
|
||||
(let* ((root (plist-get (ebox--buffer-render-state buffer)
|
||||
:root-node))
|
||||
(text (car (ebox-box-node-children root))))
|
||||
(should (eq (ebox-style-node-specified-value root :wrap-mode)
|
||||
'none))
|
||||
(should (eq (plist-get root :wrap-mode) 'none))
|
||||
(should (eq (plist-get text :wrap-mode) 'none))))
|
||||
(kill-buffer buffer))))
|
||||
|
||||
(ert-deftest ebox-box-wrap-modes-preserve-word-char-kp-and-none ()
|
||||
"Keep every established Box wrapping algorithm behind one property."
|
||||
(dolist (mode '(word char kp none))
|
||||
(let ((node (ebox-build `(box :wrap-mode ,mode "A B"))))
|
||||
(should (eq (ebox-style-node-specified-value node :wrap-mode) mode))))
|
||||
(should-error (ebox-build '(box :wrap-mode arbitrary "A")) :type 'error)
|
||||
(let* ((width (ebox--string-pixel-width "A"))
|
||||
(char-box
|
||||
(ebox-box-create
|
||||
:layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-text-create :value "AB"))
|
||||
:width (list width) :wrap-mode 'char))
|
||||
(none-box
|
||||
(ebox-box-create
|
||||
:layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-text-create :value "AB"))
|
||||
:width (list width) :wrap-mode 'none)))
|
||||
(should (= (length (ebox-string-lines (ebox-render char-box))) 2))
|
||||
(should (= (length (ebox-string-lines (ebox-render none-box))) 1)))
|
||||
(let (justify-args)
|
||||
(cl-letf (((symbol-function 'require)
|
||||
(lambda (feature &optional _filename _noerror)
|
||||
(eq feature 'ekp)))
|
||||
((symbol-function 'ekp-pixel-justify)
|
||||
(lambda (text width)
|
||||
(setq justify-args (list text width))
|
||||
text)))
|
||||
(ebox-render
|
||||
(ebox-box-create
|
||||
:layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-text-create :value "paragraph"))
|
||||
:width '(100) :wrap-mode 'kp)))
|
||||
(should (equal justify-args '("paragraph" 100)))))
|
||||
|
||||
(ert-deftest ebox-canonical-normal-box-renders-inline-runs-and-blocks ()
|
||||
"Normal should retain typed children and honor inline/block boundaries."
|
||||
(let* ((text (ebox-text-create :value "A" :source-handle 'source-text))
|
||||
@ -442,29 +527,6 @@
|
||||
:column-gap)
|
||||
8)))))
|
||||
|
||||
(ert-deftest ebox-canonical-flex-projects-one-box-frame-constraint ()
|
||||
"Typed and legacy Flex should derive the same content size from BoxFrame."
|
||||
(dolist (sizing '(border-box content-box))
|
||||
(let* ((typed
|
||||
(ebox-box-create
|
||||
:layout (ebox-flex-layout-create)
|
||||
:children (list (ebox-text-create :value "A")
|
||||
(ebox-text-create :value "B"))
|
||||
:width '(140) :padding '(0 (10)) :box-sizing sizing))
|
||||
(legacy
|
||||
(ebox-flex
|
||||
:width '(140) :padding '(0 (10)) :box-sizing sizing
|
||||
(ebox-text-create :value "A")
|
||||
(ebox-text-create :value "B")))
|
||||
(typed-rendered (ebox-render typed))
|
||||
(legacy-rendered (ebox-render legacy)))
|
||||
(should
|
||||
(string=
|
||||
(string-trim-right (substring-no-properties typed-rendered))
|
||||
(string-trim-right (substring-no-properties legacy-rendered))))
|
||||
(should (= (ebox--string-pixel-width typed-rendered)
|
||||
(ebox--string-pixel-width legacy-rendered))))))
|
||||
|
||||
(ert-deftest ebox-canonical-grid-box-uses-one-box-identity-and-child-list ()
|
||||
"A GridConfig should select the Box algorithm without a Grid runtime node."
|
||||
(let* ((layout (ebox-grid-layout-create))
|
||||
@ -573,39 +635,6 @@
|
||||
(ebox-style-add-rule ".detached-flex-item" (list :grid-row 1))
|
||||
(let ((ebox--render-root-parent-kind 'flex))
|
||||
(should-error (ebox-render detached) :type 'error)))
|
||||
;; A real legacy adapter exempts only its two exact duplicate nodes.
|
||||
(let* ((illegal-grandchild (child :flex-grow 1))
|
||||
(nested (parent (ebox-normal-layout-create) illegal-grandchild))
|
||||
(adapter (ebox-flex-item nested :flex-grow 1 :padding 1)))
|
||||
;; The obsolete mutable depth field must have no authority.
|
||||
(plist-put adapter :ebox-flex-item-adapter-depth 99)
|
||||
(should-error
|
||||
(ebox-render (ebox-flex :width '(100) adapter))
|
||||
:type 'error))
|
||||
;; Provenance binds the exact constructor-time source identities.
|
||||
(let* ((adapter (ebox-flex-item (ebox-create :content "original")))
|
||||
(replacement (ebox-create :content "replacement")))
|
||||
(plist-put replacement :flex-grow 1)
|
||||
(plist-put adapter :node replacement)
|
||||
(should-error
|
||||
(ebox-render (ebox-column adapter)) :type 'error))
|
||||
(let* ((adapter (ebox-flex-item (ebox-create :content "original")))
|
||||
(source (plist-get adapter :node))
|
||||
(replacement (ebox-create :content "replacement")))
|
||||
(plist-put replacement :flex-grow 1)
|
||||
(setq adapter
|
||||
(ebox-tree-copy-with-direct-child-replacements
|
||||
adapter (list (cons source replacement))))
|
||||
(should-error
|
||||
(ebox-render (ebox-column adapter)) :type 'error))
|
||||
(let* ((adapter
|
||||
(ebox-flex-item (ebox-create :content "original") :padding 1))
|
||||
(wrapper (plist-get adapter :node))
|
||||
(replacement (ebox-create :content "replacement")))
|
||||
(plist-put replacement :flex-grow 1)
|
||||
(plist-put wrapper :ebox-content-node replacement)
|
||||
(should-error
|
||||
(ebox-render (ebox-column adapter)) :type 'error))
|
||||
;; Internal layout and parent participation are orthogonal facts.
|
||||
(should
|
||||
(stringp
|
||||
@ -632,30 +661,6 @@
|
||||
(ebox-box-create :layout cross-kind-props :children nil)
|
||||
:type 'error)))
|
||||
|
||||
(ert-deftest ebox-canonical-grid-projects-one-box-frame-constraint ()
|
||||
"Typed and legacy Grid should derive the same content size from BoxFrame."
|
||||
(dolist (sizing '(border-box content-box))
|
||||
(let* ((typed
|
||||
(ebox-box-create
|
||||
:layout (ebox-grid-layout-create)
|
||||
:children (list (ebox-text-create :value "A")
|
||||
(ebox-text-create :value "B"))
|
||||
:width '(140) :padding '(0 (10)) :box-sizing sizing))
|
||||
(legacy
|
||||
(ebox-grid
|
||||
:width '(140) :padding '(0 (10)) :box-sizing sizing
|
||||
(ebox-text-create :value "A")
|
||||
(ebox-text-create :value "B")))
|
||||
(typed-rendered (ebox-render typed))
|
||||
(legacy-rendered (ebox-render legacy)))
|
||||
(should (string= (substring-no-properties typed-rendered)
|
||||
(substring-no-properties legacy-rendered)))
|
||||
(should
|
||||
(equal (mapcar #'ebox--string-pixel-width
|
||||
(ebox-string-lines typed-rendered))
|
||||
(mapcar #'ebox--string-pixel-width
|
||||
(ebox-string-lines legacy-rendered)))))))
|
||||
|
||||
(ert-deftest ebox-grid-layout-create-normalizes-the-complete-config-schema ()
|
||||
"The typed Grid constructor should emit one canonical closed property set."
|
||||
(let* ((layout
|
||||
@ -837,454 +842,126 @@
|
||||
(ebox-box-create :layout normal :outer nil :children nil)
|
||||
:type 'error)))
|
||||
|
||||
(ert-deftest ebox-build-normalizes-text-author-forms ()
|
||||
"The Ebox DSL should normalize explicit and bare Text forms."
|
||||
(let ((text (ebox-build '(text "A")))
|
||||
(bare (ebox-build "B")))
|
||||
(should (ebox-text-node-p text))
|
||||
(should (ebox-text-node-p bare))
|
||||
(should (string= (ebox-dsl-test--plain text) "A"))
|
||||
(should (string= (ebox-dsl-test--plain bare) "B"))))
|
||||
(ert-deftest ebox-build-normalizes-the-complete-author-grammar ()
|
||||
"Every public author form should lower to one canonical Text/Box model."
|
||||
(dolist (case
|
||||
'(("plain" text nil)
|
||||
((text "styled") text nil)
|
||||
((box) box normal)
|
||||
((row "A" "B") box row)
|
||||
((column "A" "B") box column)
|
||||
((flex "A" "B") box flex)
|
||||
((grid "A" "B") box grid)))
|
||||
(let* ((form (nth 0 case))
|
||||
(kind (nth 1 case))
|
||||
(layout-kind (nth 2 case))
|
||||
(node (ebox-build form)))
|
||||
(should (eq (ebox-node-kind node) kind))
|
||||
(should (symbolp (ebox-node-source-handle node)))
|
||||
(should-not (intern-soft
|
||||
(symbol-name (ebox-node-source-handle node))))
|
||||
(when layout-kind
|
||||
(should
|
||||
(eq (ebox-layout-config-kind (ebox-box-node-layout node))
|
||||
layout-kind)))))
|
||||
(let ((inline-row (ebox-build '(row :outer inline "A" "B"))))
|
||||
(should (equal (ebox--computed-display inline-row) '(inline row)))))
|
||||
|
||||
(ert-deftest ebox-build-text-requires-one-string-payload ()
|
||||
"The Ebox Text form should reject empty, multiple, and nested payloads."
|
||||
(should-error (ebox-build '(text)) :type 'error)
|
||||
(should-error (ebox-build '(text "A" "B")) :type 'error)
|
||||
(should-error (ebox-build '(text (text "A"))) :type 'error)
|
||||
(should-error (ebox-build '(text :color "red" "A")) :type 'error)
|
||||
(should-error (ebox-build '(text :wrap-mode garbage "A")) :type 'error))
|
||||
(ert-deftest ebox-build-projects-source-style-without-polluting-canonical-geometry ()
|
||||
"Source metadata and author paint should use their orthogonal projections."
|
||||
(let* ((form
|
||||
'(box :key root :id "root" :class "card"
|
||||
:background-color "red" :width '(80)
|
||||
(text :color "blue" "A")))
|
||||
(original (symbol-function 'ebox-style-compile-declarations))
|
||||
(compile-calls 0)
|
||||
(node
|
||||
(cl-letf (((symbol-function 'ebox-style-compile-declarations)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf compile-calls)
|
||||
(apply original arguments))))
|
||||
(ebox-build form)))
|
||||
(child (car (ebox-box-node-children node)))
|
||||
(rendered (ebox-render node))
|
||||
(face (get-text-property 0 'face rendered)))
|
||||
(should (eq (plist-get node :key) 'root))
|
||||
(should (equal (plist-get node :id) "root"))
|
||||
(should (equal (plist-get node :class) "card"))
|
||||
(should (eq (plist-get node :host-ref)
|
||||
(ebox-node-source-handle node)))
|
||||
(should (eq (plist-get child :host-ref)
|
||||
(ebox-node-source-handle child)))
|
||||
(should (= compile-calls 2))
|
||||
(should (equal (ebox-get node :bgcolor) nil))
|
||||
(should
|
||||
(cl-some (lambda (entry)
|
||||
(and (listp entry)
|
||||
(equal (plist-get entry :background) "red")))
|
||||
face))
|
||||
(should
|
||||
(cl-some (lambda (entry)
|
||||
(and (listp entry)
|
||||
(equal (plist-get entry :foreground) "blue")))
|
||||
face))))
|
||||
(ert-deftest ebox-build-gives-equal-forms-distinct-source-identities ()
|
||||
"Structurally equal author forms must not share opaque source identity."
|
||||
(let* ((node (ebox-build '(row (box "same") (box "same"))))
|
||||
(children (ebox-box-node-children node)))
|
||||
(should-not (equal (plist-get (car children) :host-ref)
|
||||
(plist-get (cadr children) :host-ref)))))
|
||||
|
||||
(ert-deftest ebox-dsl-module-preserves-public-build-syntax ()
|
||||
"Extracted DSL compiler should preserve existing ebox-build syntax."
|
||||
(let ((node (ebox-build
|
||||
'(column :padding 1 :bgcolor "linen"
|
||||
(row
|
||||
(box :content "A" :width (40))
|
||||
(box :content "B" :width (40)))))))
|
||||
(should (eq (plist-get node :ebox-type) 'box))
|
||||
(should (string-match-p "A" (ebox-render node)))
|
||||
(should (string-match-p "B" (ebox-render node)))))
|
||||
(ert-deftest ebox-build-validates-form-owned-config-and-direct-participation ()
|
||||
"Layout config belongs to its form; item roles belong to direct child Boxes."
|
||||
(let* ((flex
|
||||
(ebox-build
|
||||
'(flex :width '(300) :gap '(1 (5))
|
||||
(box :flex 1 "A")
|
||||
"B")))
|
||||
(flex-child (car (ebox-box-node-children flex)))
|
||||
(grid
|
||||
(ebox-build
|
||||
'(grid :grid-template-columns '((20) (20))
|
||||
(box :grid-column '(1 :span 2) "Header"))))
|
||||
(grid-child (car (ebox-box-node-children grid))))
|
||||
(should (= (plist-get flex-child :flex-grow) 1))
|
||||
(should (equal (plist-get grid-child :grid-column) '(1 :span 2)))
|
||||
(should (string-match-p "A.*B"
|
||||
(substring-no-properties (ebox-render flex))))
|
||||
(should (string-match-p "Header"
|
||||
(substring-no-properties (ebox-render grid)))))
|
||||
(dolist (form
|
||||
'((box :flex-direction row)
|
||||
(row :gap 1)
|
||||
(column :grid-template-columns ((10)))
|
||||
(flex :grid-template-columns ((10)))
|
||||
(grid :flex-direction row)))
|
||||
(should-error (ebox-build form) :type 'error))
|
||||
(should-error
|
||||
(ebox-render (ebox-build '(box :flex-grow 1 "wrong parent")))
|
||||
:type 'error))
|
||||
|
||||
(ert-deftest ebox-build-creates-box-from-keyword-plist ()
|
||||
"A `(box ...)' form should compile to a normal Ebox box node."
|
||||
(let ((node (ebox-build '(box :content "Hello" :width (120) :border t))))
|
||||
(should (eq (plist-get node :ebox-type) 'box))
|
||||
(should (string-match-p "Hello" (ebox-dsl-test--plain node)))))
|
||||
|
||||
(ert-deftest ebox-build-uses-string-child-as-box-content ()
|
||||
"A simple string child should be shorthand for box content."
|
||||
(let ((node (ebox-build '(box :width (120) "Hello"))))
|
||||
(should (eq (plist-get node :ebox-type) 'box))
|
||||
(should (string-match-p "Hello" (ebox-dsl-test--plain node)))))
|
||||
|
||||
(ert-deftest ebox-build-resolves-viewport-width-from-dynamic-binding ()
|
||||
"The DSL should allow box widths to follow the current render viewport."
|
||||
(let* ((ebox-viewport-width 320)
|
||||
(node (ebox-build
|
||||
'(box :content "Viewport"
|
||||
:width (viewport)
|
||||
:box-sizing border-box
|
||||
:border t))))
|
||||
(should (= (car (ebox-dsl-test--line-widths node)) 320))))
|
||||
|
||||
(ert-deftest ebox-build-resolves-viewport-height-from-dynamic-binding ()
|
||||
"The DSL should allow box heights to follow the current render viewport."
|
||||
(let* ((ebox-viewport-height 3)
|
||||
(node (ebox-build
|
||||
'(box :content "One\nTwo\nThree\nFour"
|
||||
:height (viewport-height)
|
||||
:overflow scroll))))
|
||||
(should (= (length (ebox-string-lines (ebox-render node))) 3))))
|
||||
|
||||
(ert-deftest ebox-build-resolves-viewport-height-arithmetic-expression ()
|
||||
"The DSL should allow viewport-relative height expressions."
|
||||
(let* ((ebox-viewport-height 6)
|
||||
(node (ebox-build
|
||||
'(box :content "One\nTwo\nThree\nFour"
|
||||
:height (- (viewport-height) 2)
|
||||
:overflow scroll))))
|
||||
(should (= (length (ebox-string-lines (ebox-render node))) 4))))
|
||||
|
||||
(ert-deftest ebox-build-unquotes-static-list-property-values ()
|
||||
"The DSL should accept quoted list values used by `etaf-view' forms."
|
||||
(let* ((node (ebox-build
|
||||
'(grid :width '(120)
|
||||
:grid-template-columns '((40) 1fr)
|
||||
:gap '(0 (4))
|
||||
(box :content "A")
|
||||
(box :content "B"))))
|
||||
(props (plist-get node :raw-props))
|
||||
(line (car (ebox-string-lines (ebox-render node)))))
|
||||
(should (equal (plist-get props :width) '(120)))
|
||||
(should (equal (plist-get props :grid-template-columns)
|
||||
'((40) 1fr)))
|
||||
(should (= (ebox--string-pixel-width line) 120))))
|
||||
|
||||
(ert-deftest ebox-build-composes-fixed-viewport-shell-around-scrollbox ()
|
||||
"A fixed viewport shell can reserve chrome while an inner box scrolls."
|
||||
(let* ((ebox-viewport-height 6)
|
||||
(node (ebox-build
|
||||
'(column :height (viewport-height)
|
||||
(box :content "top" :height 1)
|
||||
(box :id "root" :height (- (viewport-height) 2)
|
||||
:overflow scroll
|
||||
(column
|
||||
(box :content "A" :height 1)
|
||||
(box :content "B" :height 1)
|
||||
(box :content "C" :height 1)
|
||||
(box :content "D" :height 1)
|
||||
(box :content "E" :height 1)))
|
||||
(box :content "bottom" :height 1)))))
|
||||
(should (= (length (ebox-string-lines (ebox-render node))) 6))))
|
||||
|
||||
(ert-deftest ebox-build-composes-row-and-column-layouts ()
|
||||
"Row and column forms should compose existing lazy layout nodes."
|
||||
(let* ((node (ebox-build
|
||||
'(column
|
||||
(box :content "Header" :width (180))
|
||||
(row
|
||||
(box :content "Left" :width (80))
|
||||
(spacer :width (20))
|
||||
(box :content "Right" :width (80))))))
|
||||
(plain (ebox-dsl-test--plain node)))
|
||||
(should (eq (plist-get node :ebox-type) 'stack))
|
||||
(should (string-match-p "Header" plain))
|
||||
(should (string-match-p "Left.*Right" plain))
|
||||
(should (= (length (ebox-region-ids node)) 4))))
|
||||
|
||||
(ert-deftest ebox-build-wraps-child-layout-with-box-properties ()
|
||||
"A node with child layout may still apply box properties to the wrapper."
|
||||
(let* ((node (ebox-build
|
||||
'(box :width (240) :box-sizing border-box
|
||||
:padding (1 20) :border t :bgcolor "#222222"
|
||||
(row
|
||||
(box :content "A" :width (80))
|
||||
(box :content "B" :width (80))))))
|
||||
(plain (ebox-dsl-test--plain node)))
|
||||
(should (eq (plist-get node :ebox-type) 'box))
|
||||
(should (equal (ebox-get node :bgcolor) "#222222"))
|
||||
(should (string-match-p "A.*B" plain))
|
||||
(dolist (width (ebox-dsl-test--line-widths node))
|
||||
(should (= width 240)))))
|
||||
|
||||
(ert-deftest ebox-build-box-wrapper-preserves-preformatted-child-layout ()
|
||||
"A box wrapping child layout should not rewrap rendered children."
|
||||
(cl-letf (((symbol-function 'require)
|
||||
(lambda (feature &optional _filename _noerror)
|
||||
(when (eq feature 'ekp) t)))
|
||||
((symbol-function 'ekp-pixel-justify)
|
||||
(lambda (text _width)
|
||||
(if (string-match-p "\n" text)
|
||||
"REWRAPPED"
|
||||
text))))
|
||||
(let* ((node (ebox-build
|
||||
'(box :width (220) :box-sizing border-box
|
||||
(row
|
||||
(box :content "Left" :width (80) :height 2)
|
||||
(box :content "Right" :width (80) :height 2)))))
|
||||
(plain (ebox-dsl-test--plain node)))
|
||||
(should-not (string-match-p "REWRAPPED" plain))
|
||||
(should (string-match-p "Left.*Right" plain))
|
||||
(dolist (width (ebox-dsl-test--line-widths node))
|
||||
(should (= width 220))))))
|
||||
|
||||
(ert-deftest ebox-build-row-wrapper-preserves-preformatted-layout ()
|
||||
"Row box properties should not rewrap the rendered child layout."
|
||||
(cl-letf (((symbol-function 'require)
|
||||
(lambda (feature &optional _filename _noerror)
|
||||
(when (eq feature 'ekp) t)))
|
||||
((symbol-function 'ekp-pixel-justify)
|
||||
(lambda (text _width)
|
||||
(if (string-match-p "\n" text)
|
||||
"REWRAPPED"
|
||||
text))))
|
||||
(let* ((node (ebox-build
|
||||
'(row :width (220) :box-sizing border-box
|
||||
(box :content "Left" :width (80) :height 2)
|
||||
(box :content "Right" :width (80) :height 2))))
|
||||
(plain (ebox-dsl-test--plain node)))
|
||||
(should-not (string-match-p "REWRAPPED" plain))
|
||||
(should (string-match-p "Left.*Right" plain))
|
||||
(dolist (width (ebox-dsl-test--line-widths node))
|
||||
(should (= width 220))))))
|
||||
|
||||
(ert-deftest ebox-build-column-wrapper-preserves-preformatted-layout ()
|
||||
"Column box properties should not rewrap the rendered child layout."
|
||||
(cl-letf (((symbol-function 'require)
|
||||
(lambda (feature &optional _filename _noerror)
|
||||
(when (eq feature 'ekp) t)))
|
||||
((symbol-function 'ekp-pixel-justify)
|
||||
(lambda (text _width)
|
||||
(if (string-match-p "\n" text)
|
||||
"REWRAPPED"
|
||||
text))))
|
||||
(let* ((node (ebox-build
|
||||
'(column :width (180) :box-sizing border-box
|
||||
(box :content "Top" :width (120))
|
||||
(box :content "Bottom" :width (120)))))
|
||||
(plain (ebox-dsl-test--plain node)))
|
||||
(should-not (string-match-p "REWRAPPED" plain))
|
||||
(should (string-match-p "Top" plain))
|
||||
(should (string-match-p "Bottom" plain))
|
||||
(dolist (width (ebox-dsl-test--line-widths node))
|
||||
(should (= width 180))))))
|
||||
|
||||
(ert-deftest ebox-build-padded-column-wrapper-stays-inside-viewport ()
|
||||
"A padded layout wrapper should not let preformatted children overflow."
|
||||
(let* ((ebox-viewport-width 320)
|
||||
(node (ebox-build
|
||||
'(column :padding 1
|
||||
(box :content "Child width follows the viewport"
|
||||
:wrap-mode word))))
|
||||
(widths (ebox-dsl-test--display-line-widths node)))
|
||||
(should (cl-every (lambda (width)
|
||||
(<= width ebox-viewport-width))
|
||||
widths))))
|
||||
|
||||
(ert-deftest ebox-build-wrapper-explicit-width-bounds-child-layout-viewport ()
|
||||
"A wrapper's explicit width should be the containing block for child layouts."
|
||||
(let* ((ebox-viewport-width 360)
|
||||
(node (ebox-build
|
||||
'(column :width (180)
|
||||
(box :content "alpha beta gamma delta epsilon zeta eta theta"
|
||||
:wrap-mode word))))
|
||||
(widths (ebox-dsl-test--line-widths node)))
|
||||
(dolist (width widths)
|
||||
(should (<= width 180)))))
|
||||
|
||||
(ert-deftest ebox-build-column-contains-auto-row-children ()
|
||||
"A column wrapper must not let an auto-width row child duplicate its viewport."
|
||||
(let* ((node (ebox-build
|
||||
'(column :width (120)
|
||||
(row
|
||||
(box :content "Left" :border t)
|
||||
(box :content "Right" :border t))
|
||||
(box :content "Body" :border t))))
|
||||
(widths (ebox-dsl-test--line-widths node)))
|
||||
(should (cl-every (lambda (width) (<= width 120)) widths))))
|
||||
|
||||
(ert-deftest ebox-row-keeps-explicit-viewport-widths-intrinsic-context ()
|
||||
"Row intrinsic sizing must not change explicit viewport-sized children."
|
||||
(let* ((ebox-viewport-width 120)
|
||||
(node (ebox-row
|
||||
(ebox-create :content "Left" :width 'viewport)
|
||||
(ebox-create :content "Right" :width 'stretch)))
|
||||
(widths (ebox-dsl-test--line-widths node)))
|
||||
(should (equal widths '(240)))))
|
||||
|
||||
(ert-deftest ebox-row-keeps-composite-column-intrinsic-width ()
|
||||
"A composite column child must not consume the row viewport as auto width."
|
||||
(let* ((ebox-viewport-width 120)
|
||||
(node (ebox-row
|
||||
(ebox-column
|
||||
(ebox-create :content "Top")
|
||||
(ebox-create :content "Bottom"))
|
||||
(ebox-create :content "Tail")))
|
||||
(widths (ebox-dsl-test--line-widths node)))
|
||||
(should (cl-every (lambda (width) (<= width 120)) widths))))
|
||||
|
||||
(ert-deftest ebox-build-lazy-column-contains-auto-row-children ()
|
||||
"Lazy column windows must apply the same intrinsic row sizing rule."
|
||||
(let* ((ebox-viewport-width 120)
|
||||
(ebox-viewport-height 4)
|
||||
(node (ebox-build
|
||||
'(box :width (viewport)
|
||||
:height (viewport-height)
|
||||
:overflow scroll
|
||||
(column
|
||||
(row
|
||||
(box :content "Left" :border t)
|
||||
(box :content "Right" :border t))
|
||||
(box :content "Body" :border t)))))
|
||||
(widths (ebox-dsl-test--line-widths node)))
|
||||
(should (cl-every (lambda (width) (<= width 120)) widths))))
|
||||
|
||||
(ert-deftest ebox-build-wrapper-viewport-width-bounds-child-layout-viewport ()
|
||||
"A wrapper's `(viewport)' width should pass its content box to child layouts."
|
||||
(let ((cases
|
||||
'((box :width (viewport)
|
||||
:padding-inline (20)
|
||||
:border-left (10)
|
||||
:border-right (10)
|
||||
(box :content "alpha beta gamma delta epsilon zeta eta theta"
|
||||
:wrap-mode word))
|
||||
(column :width (viewport)
|
||||
:padding-inline (20)
|
||||
:border-left (10)
|
||||
:border-right (10)
|
||||
(box :content "alpha beta gamma delta epsilon zeta eta theta"
|
||||
:wrap-mode word))
|
||||
(row :width (viewport)
|
||||
:padding-inline (20)
|
||||
:border-left (10)
|
||||
:border-right (10)
|
||||
(box :content "alpha beta gamma delta epsilon zeta eta theta"
|
||||
:wrap-mode word)
|
||||
(spacer :width (0) :height 1)))))
|
||||
(dolist (dsl cases)
|
||||
(let* ((ebox-viewport-width 240)
|
||||
(node (ebox-build dsl))
|
||||
(widths (ebox-dsl-test--line-widths node)))
|
||||
(dolist (width widths)
|
||||
(should (<= width ebox-viewport-width)))))))
|
||||
|
||||
(ert-deftest ebox-build-lazy-scroll-prefix-bounds-wrapper-child-viewport ()
|
||||
"Lazy scroll prefix rendering should preserve wrapper content viewport."
|
||||
(let* ((ebox-viewport-width 320)
|
||||
(ebox-viewport-height 5)
|
||||
(node (ebox-build
|
||||
'(box :id "root"
|
||||
:width (viewport)
|
||||
:height (- (viewport-height) 1)
|
||||
:box-sizing border-box
|
||||
:overflow scroll
|
||||
(column :width (viewport)
|
||||
:box-sizing border-box
|
||||
:padding (1 (18))
|
||||
(box :content "One"
|
||||
:width (viewport)
|
||||
:box-sizing border-box
|
||||
:border t)
|
||||
(box :content "Two"
|
||||
:width (viewport)
|
||||
:box-sizing border-box
|
||||
:border t)
|
||||
(box :content "Three"
|
||||
:width (viewport)
|
||||
:box-sizing border-box
|
||||
:border t)
|
||||
(box :content "Four"
|
||||
:width (viewport)
|
||||
:box-sizing border-box
|
||||
:border t)))))
|
||||
(widths (ebox-dsl-test--line-widths node)))
|
||||
(dolist (width widths)
|
||||
(should (<= width ebox-viewport-width)))))
|
||||
|
||||
(ert-deftest ebox-build-tags-map-to-css-like-display ()
|
||||
"DSL container tags should expose CSS-like outer/inner display values."
|
||||
(let ((box-node (ebox-build '(box :content "A")))
|
||||
(row-node (ebox-build '(row (box :content "A") (box :content "B"))))
|
||||
(column-node (ebox-build
|
||||
'(column (box :content "A") (box :content "B"))))
|
||||
(flex-node (ebox-build '(flex (box :content "A") (box :content "B")))))
|
||||
(should (equal (ebox--computed-display box-node) '(block flow)))
|
||||
(should (equal (ebox--computed-display row-node) '(block row)))
|
||||
(should (equal (ebox--computed-display column-node) '(block column)))
|
||||
(should (equal (ebox--computed-display flex-node) '(block flex)))))
|
||||
|
||||
(ert-deftest ebox-build-box-uses-orthogonal-outer-and-layout-axes ()
|
||||
"Canonical Box DSL should select outer participation and child layout."
|
||||
(let ((normal (ebox-build '(box :outer inline :layout normal "A")))
|
||||
(row (ebox-build
|
||||
'(box :outer inline :layout row
|
||||
(box "A")
|
||||
(box "B"))))
|
||||
(column (ebox-build
|
||||
'(box :outer block :layout column
|
||||
(box "A")
|
||||
(box "B")))))
|
||||
(should (equal (ebox--computed-display normal) '(inline flow)))
|
||||
(should (equal (ebox--computed-display row) '(inline row)))
|
||||
(should (equal (ebox--computed-display column) '(block column)))
|
||||
(should (string= (ebox-dsl-test--plain row) "AB"))
|
||||
(should (string= (ebox-dsl-test--plain column) "A\nB"))))
|
||||
|
||||
(ert-deftest ebox-build-box-rejects-invalid-axis-values ()
|
||||
"Canonical Box DSL should reject unknown outer and layout values."
|
||||
(should-error (ebox-build '(box :outer floating "A")) :type 'error)
|
||||
(should-error (ebox-build '(box :layout masonry "A")) :type 'error))
|
||||
|
||||
(ert-deftest ebox-build-box-layout-consumes-direct-flex-item-properties ()
|
||||
"Canonical Flex Box should consume participation props from child Boxes."
|
||||
(let* ((node (ebox-build
|
||||
'(box :layout flex :width (300)
|
||||
(box :flex-grow 1 :flex-basis (80) "A")
|
||||
(box :flex-grow 2 :flex-basis (80) "B"))))
|
||||
(children (plist-get node :children)))
|
||||
(should (equal (ebox--computed-display node) '(block flex)))
|
||||
(should (= (plist-get (car children) :flex-grow) 1))
|
||||
(should (= (plist-get (cadr children) :flex-grow) 2))
|
||||
(should (string-match-p "A" (ebox-dsl-test--plain node)))
|
||||
(should (string-match-p "B" (ebox-dsl-test--plain node)))))
|
||||
|
||||
(ert-deftest ebox-build-box-layout-consumes-direct-grid-item-properties ()
|
||||
"Canonical Grid Box should consume placement props from child Boxes."
|
||||
(let* ((node (ebox-build
|
||||
'(box :layout grid :grid-template-columns ((20) (20))
|
||||
(box :grid-column (1 :span 2) "Header"))))
|
||||
(child (car (plist-get node :children))))
|
||||
(should (equal (ebox--computed-display node) '(block grid)))
|
||||
(should (equal (plist-get child :grid-column) '(1 :span 2)))
|
||||
(should (string-match-p "Header" (ebox-dsl-test--plain node)))))
|
||||
|
||||
(ert-deftest ebox-build-wrapper-child-layout-rerenders-on-runtime-viewport-change ()
|
||||
"A DSL wrapper box should rerender child layout content on viewport resize."
|
||||
(let ((node (let ((ebox-viewport-width 96))
|
||||
(ebox-build
|
||||
'(column :padding 1 :bgcolor "linen"
|
||||
(box :content "This child text should wrap again when the preview becomes narrow."
|
||||
:wrap-mode word)))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(setq buffer
|
||||
(let ((ebox-viewport-width 96))
|
||||
(ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-dsl-test*")
|
||||
node)))
|
||||
(let ((wide-text (with-current-buffer buffer
|
||||
(buffer-substring-no-properties
|
||||
(point-min) (point-max))))
|
||||
(wide-height (with-current-buffer buffer
|
||||
(count-lines (point-min) (point-max)))))
|
||||
(ebox-rerender-buffer-with-context buffer 36)
|
||||
(let ((narrow-text (with-current-buffer buffer
|
||||
(buffer-substring-no-properties
|
||||
(point-min) (point-max))))
|
||||
(narrow-height (with-current-buffer buffer
|
||||
(count-lines (point-min) (point-max)))))
|
||||
(should-not (string= wide-text narrow-text))
|
||||
(should (> narrow-height wide-height)))))
|
||||
(when (and buffer (buffer-live-p buffer))
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-build-item-is-flex-participation-sugar ()
|
||||
"DSL item should attach flex participation to its child semantics."
|
||||
(let* ((node (ebox-build
|
||||
'(flex :width (300)
|
||||
(item :flex 1 (box :content "A"))
|
||||
(box :content "B" :flex 1))))
|
||||
(rendered (let ((ebox-viewport-width 300))
|
||||
(ebox-render node))))
|
||||
(should (string-match-p "A" (substring-no-properties rendered)))
|
||||
(should (string-match-p "B" (substring-no-properties rendered)))))
|
||||
|
||||
(ert-deftest ebox-build-keeps-pre-render-region-ids-available ()
|
||||
"The DSL should not break the pre-render region-id workflow."
|
||||
(let* ((node (ebox-build
|
||||
'(row
|
||||
(box :content "One" :width (80))
|
||||
(box :content "Two" :width (80)))))
|
||||
(ids (ebox-region-ids node)))
|
||||
(should (= (length ids) 2))
|
||||
(should (cl-every #'identity ids))))
|
||||
|
||||
(ert-deftest ebox-build-wrapper-region-ids-include-child-layout ()
|
||||
"A wrapper box should keep both its own region id and child layout ids."
|
||||
(let* ((node (ebox-build
|
||||
'(column :padding 1
|
||||
(box :content "One")
|
||||
(box :content "Two"))))
|
||||
(ids (ebox-region-ids node)))
|
||||
(should (= (length ids) 3))
|
||||
(should (cl-every #'identity ids))))
|
||||
|
||||
(ert-deftest ebox-build-rejects-invalid-dsl ()
|
||||
"Bad forms should fail loudly instead of producing surprising nodes."
|
||||
(should-error (ebox-build '(unknown :content "Nope")) :type 'error)
|
||||
(should-error (ebox-build '(box :content)) :type 'error)
|
||||
(should-error (ebox-build '(spacer :width (20) (box :content "bad"))) :type 'error))
|
||||
(ert-deftest ebox-build-rejects-removed-and-private-author-syntax ()
|
||||
"The author boundary should have one grammar and no runtime passthrough."
|
||||
(dolist (form
|
||||
'((ebox "A")
|
||||
(spacer :width (20))
|
||||
(item (box "A"))
|
||||
(grid-item (box "A"))
|
||||
(box :content "A")
|
||||
(box :layout flex "A")
|
||||
(box :display (block flex) "A")
|
||||
(box :ebox-type box "A")
|
||||
(box :ebox-content-node nil)
|
||||
(text)
|
||||
(text "A" "B")
|
||||
(text (box "A"))
|
||||
(text :width (10) "A")
|
||||
(row :item-gap -1)
|
||||
(unknown "A")))
|
||||
(should-error (ebox-build form) :type 'error))
|
||||
(should-error
|
||||
(ebox-build (ebox-test-box :content "raw runtime node"))
|
||||
:type 'error))
|
||||
|
||||
(provide 'ebox-dsl-tests)
|
||||
;;; ebox-dsl-tests.el ends here
|
||||
|
||||
253
tests/ebox-fixtures.el
Normal file
253
tests/ebox-fixtures.el
Normal file
@ -0,0 +1,253 @@
|
||||
;;; ebox-fixtures.el --- Typed Ebox test fixtures -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
;; Test-only construction helpers. They keep retained layout, publication,
|
||||
;; and rollback tests on the canonical Text/Box/LayoutConfig model without
|
||||
;; reintroducing removed public constructors.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'ebox)
|
||||
|
||||
(defconst ebox-test--source-fields
|
||||
'(:source-handle :key :class :id)
|
||||
"Canonical source fields accepted by typed fixture nodes.")
|
||||
|
||||
(defconst ebox-test--direct-fields
|
||||
'(:region-id :selector-state :selector-attributes :surface-properties
|
||||
:scroll-offset :ebox-scroll-offset-controlled-p)
|
||||
"Private runtime fields used only by focused backend tests.")
|
||||
|
||||
(defconst ebox-test--non-style-fields
|
||||
(append '(:source-handle :host-ref :key :class :id
|
||||
:outer :content :ebox-content-node
|
||||
:item-gap :cross-align)
|
||||
ebox-test--direct-fields)
|
||||
"Fixture fields that do not enter the canonical Ebox style schema.")
|
||||
|
||||
(defun ebox-test--keep (plist keys)
|
||||
"Return PLIST entries whose keys occur in KEYS."
|
||||
(cl-loop for (key value) on plist by #'cddr
|
||||
when (memq key keys) append (list key value)))
|
||||
|
||||
(defun ebox-test--split-items (items)
|
||||
"Return `(PROPERTIES . CHILDREN)' from evaluated fixture ITEMS."
|
||||
(let (properties children)
|
||||
(while items
|
||||
(let ((item (pop items)))
|
||||
(if (keywordp item)
|
||||
(progn
|
||||
(unless items (error "Missing fixture value for %S" item))
|
||||
(setq properties
|
||||
(append properties (list item (pop items)))))
|
||||
(push item children))))
|
||||
(cons properties (nreverse children))))
|
||||
|
||||
(defun ebox-test--without (plist keys)
|
||||
"Return PLIST without entries whose keys occur in KEYS."
|
||||
(cl-loop for (key value) on plist by #'cddr
|
||||
unless (memq key keys) append (list key value)))
|
||||
|
||||
(defun ebox-test--unquote (value)
|
||||
"Return VALUE without one inert source-level quote wrapper."
|
||||
(if (and (consp value)
|
||||
(eq (car value) 'quote)
|
||||
(null (cddr value)))
|
||||
(cadr value)
|
||||
value))
|
||||
|
||||
(defun ebox-test--normalize-properties (props)
|
||||
"Return fixture PROPS with source literals normalized.
|
||||
The typed author boundary spells explicit no-wrap as `none'; old backend
|
||||
fixtures used nil for the same engine condition."
|
||||
(cl-loop for (key value) on props by #'cddr
|
||||
append (list key
|
||||
(if (and (eq key :wrap-mode) (null value))
|
||||
'none
|
||||
(ebox-test--unquote value)))))
|
||||
|
||||
(defun ebox-test--source-properties (props)
|
||||
"Return typed source metadata projected from fixture PROPS."
|
||||
(let ((source (ebox-test--keep props '(:key :class :id))))
|
||||
(when-let* ((handle (or (plist-get props :source-handle)
|
||||
(plist-get props :host-ref))))
|
||||
(setq source (append source (list :source-handle handle))))
|
||||
source))
|
||||
|
||||
(defun ebox-test--declarations (tag props)
|
||||
"Return canonical TAG declarations from evaluated fixture PROPS."
|
||||
(ebox-style-compile-form
|
||||
tag (ebox-test--without props ebox-test--non-style-fields)))
|
||||
|
||||
(defun ebox-test--layout (tag props declarations)
|
||||
"Return TAG LayoutConfig from PROPS and DECLARATIONS."
|
||||
(ebox-layout-config-for-form
|
||||
tag
|
||||
(if (memq tag '(row column))
|
||||
(ebox-test--keep props '(:item-gap :cross-align))
|
||||
(ebox-style-declaration-properties
|
||||
declarations
|
||||
(lambda (property)
|
||||
(memq (plist-get property :name)
|
||||
(ebox-layout-config-property-names tag)))))))
|
||||
|
||||
(defun ebox-test--text (value &optional props)
|
||||
"Return one canonical Text fixture for VALUE and author PROPS."
|
||||
(let ((declarations (ebox-test--declarations 'text props)))
|
||||
(apply #'ebox-text-create
|
||||
(append (list :value value :declarations declarations)
|
||||
(ebox-test--source-properties props)))))
|
||||
|
||||
(defun ebox-test--box-node (tag props children)
|
||||
"Return canonical Box form TAG from PROPS and CHILDREN."
|
||||
(let* ((props (ebox-test--normalize-properties props))
|
||||
(declarations (ebox-test--declarations tag props))
|
||||
(node
|
||||
(apply #'ebox-box-create
|
||||
(append
|
||||
(list :layout (ebox-test--layout tag props declarations)
|
||||
:outer (or (plist-get props :outer) 'block)
|
||||
:children children
|
||||
:declarations declarations)
|
||||
(ebox-test--source-properties props)))))
|
||||
(dolist (key ebox-test--direct-fields)
|
||||
(when (plist-member props key)
|
||||
(plist-put node key (copy-tree (plist-get props key)))))
|
||||
node))
|
||||
|
||||
(defun ebox-test-box (&rest props)
|
||||
"Return one typed Normal Box fixture from evaluated PROPS."
|
||||
(let* ((content-present-p (plist-member props :content))
|
||||
(content (plist-get props :content))
|
||||
(child (plist-get props :ebox-content-node))
|
||||
(children
|
||||
(cond
|
||||
(child (list child))
|
||||
(content-present-p
|
||||
(if (stringp content)
|
||||
(list (ebox-test--text content))
|
||||
(error "Fixture :content must be a string: %S" content)))
|
||||
(t nil))))
|
||||
(ebox-test--box-node 'box props children)))
|
||||
|
||||
(defun ebox-test-row (&rest items)
|
||||
"Return one typed Row fixture from evaluated ITEMS."
|
||||
(pcase-let ((`(,props . ,children) (ebox-test--split-items items)))
|
||||
(ebox-test--box-node 'row props (delq nil children))))
|
||||
|
||||
(defun ebox-test-column (&rest items)
|
||||
"Return one typed Column fixture from evaluated ITEMS."
|
||||
(pcase-let ((`(,props . ,children) (ebox-test--split-items items)))
|
||||
(ebox-test--box-node 'column props (delq nil children))))
|
||||
|
||||
(defun ebox-test-concat (left right)
|
||||
"Return a two-child typed Row fixture."
|
||||
(ebox-test-row left right))
|
||||
|
||||
(defun ebox-test-stack (top bottom)
|
||||
"Return a two-child typed Column fixture."
|
||||
(ebox-test-column top bottom))
|
||||
|
||||
(defun ebox-test-spacer (&rest props)
|
||||
"Return an empty typed Normal Box fixture."
|
||||
(apply #'ebox-test-box props))
|
||||
|
||||
(defun ebox-test-flex (&rest items)
|
||||
"Return one typed Flex fixture from evaluated ITEMS."
|
||||
(pcase-let ((`(,props . ,children) (ebox-test--split-items items)))
|
||||
(ebox-test--box-node 'flex props (delq nil children))))
|
||||
|
||||
(defun ebox-test-grid (&rest items)
|
||||
"Return one typed Grid fixture from evaluated ITEMS."
|
||||
(pcase-let ((`(,props . ,children) (ebox-test--split-items items)))
|
||||
(ebox-test--box-node 'grid props (delq nil children))))
|
||||
|
||||
(defun ebox-test-grid-fr (factor)
|
||||
"Return one fractional Grid track fixture."
|
||||
(list 'fr factor))
|
||||
|
||||
(defun ebox-test-flex-item (node &rest props)
|
||||
"Return NODE with direct Flex participation and Box properties from PROPS."
|
||||
(setq props (ebox-test--normalize-properties props))
|
||||
(unless (ebox-box-node-p node)
|
||||
(setq node (ebox-test--box-node 'box nil (list node))))
|
||||
(let* ((layout (ebox-box-node-layout node))
|
||||
(children (ebox-box-node-children node))
|
||||
(base (ebox-style-node-declarations node))
|
||||
(extra (ebox-test--declarations 'box props))
|
||||
(copy
|
||||
(apply #'ebox-box-create
|
||||
(append
|
||||
(list :layout layout :children children
|
||||
:outer (ebox-tree-display-outer node)
|
||||
:declarations
|
||||
(ebox-style-merge-declarations base extra))
|
||||
(ebox-test--source-properties node)))))
|
||||
copy))
|
||||
|
||||
(defun ebox-test--build-item (items)
|
||||
"Return one typed direct-child Box from old backend fixture ITEMS.
|
||||
`item' never reaches production input; this test helper only projects its
|
||||
participation properties onto the material child being exercised."
|
||||
(pcase-let* ((`(,props . ,children) (ebox-test--split-items items))
|
||||
(props (ebox-test--normalize-properties props)))
|
||||
(unless (= (length children) 1)
|
||||
(error "Ebox test item requires exactly one child: %S" items))
|
||||
(apply #'ebox-test-flex-item
|
||||
(ebox-test-build (car children)) props)))
|
||||
|
||||
(defun ebox-test--build-box-form (tag items)
|
||||
"Return one typed TAG Box fixture from old backend fixture ITEMS."
|
||||
(pcase-let* ((`(,props . ,child-forms) (ebox-test--split-items items))
|
||||
(props (ebox-test--normalize-properties props))
|
||||
(content-present-p (plist-member props :content))
|
||||
(content (plist-get props :content))
|
||||
(content-node (plist-get props :ebox-content-node))
|
||||
(props (ebox-test--without
|
||||
props '(:content :ebox-content-node)))
|
||||
(children (mapcar #'ebox-test-build child-forms)))
|
||||
(when content-node
|
||||
(setq children (append children (list content-node))))
|
||||
(when content-present-p
|
||||
(unless (stringp content)
|
||||
(error "Ebox test :content must be a string: %S" content))
|
||||
(setq children (append children (list (ebox-test--text content)))))
|
||||
(ebox-test--box-node tag props children)))
|
||||
|
||||
(defun ebox-test-build (form)
|
||||
"Build canonical typed nodes from one backend test FORM.
|
||||
|
||||
This is deliberately not an author DSL. It isolates retained-layout and
|
||||
publication tests from source parsing while their compact historical fixture
|
||||
data is migrated. Production `ebox-build' remains strict and never accepts
|
||||
`:content', `item', `concat', `stack', or `spacer'."
|
||||
(cond
|
||||
((or (ebox-text-node-p form) (ebox-box-node-p form)) form)
|
||||
((stringp form) (ebox-test--text form))
|
||||
((not (and (consp form) (symbolp (car form))))
|
||||
(error "Invalid Ebox typed test form: %S" form))
|
||||
((eq (car form) 'text)
|
||||
(pcase-let* ((`(,props . ,payloads)
|
||||
(ebox-test--split-items (cdr form)))
|
||||
(props (ebox-test--normalize-properties props)))
|
||||
(unless (and (= (length payloads) 1)
|
||||
(stringp (car payloads)))
|
||||
(error "Ebox test text requires exactly one string payload: %S" form))
|
||||
(ebox-test--text (car payloads) props)))
|
||||
((memq (car form) '(box row column flex grid))
|
||||
(ebox-test--build-box-form (car form) (cdr form)))
|
||||
((eq (car form) 'item)
|
||||
(ebox-test--build-item (cdr form)))
|
||||
((eq (car form) 'concat)
|
||||
(ebox-test--build-box-form 'row (cdr form)))
|
||||
((eq (car form) 'stack)
|
||||
(ebox-test--build-box-form 'column (cdr form)))
|
||||
((eq (car form) 'spacer)
|
||||
(ebox-test--build-box-form 'box (cdr form)))
|
||||
(t
|
||||
(error "Unknown Ebox typed test form: %S" (car form)))))
|
||||
|
||||
(provide 'ebox-fixtures)
|
||||
|
||||
;;; ebox-fixtures.el ends here
|
||||
File diff suppressed because it is too large
Load Diff
@ -15,34 +15,34 @@
|
||||
"Grid should place unpositioned children in row-major order."
|
||||
(let ((plain
|
||||
(ebox-grid-test--plain
|
||||
(ebox-grid
|
||||
(ebox-test-grid
|
||||
:grid-template-columns '((20) (20))
|
||||
:grid-template-rows '(1 1)
|
||||
:gap '(0 (1))
|
||||
(ebox-create :content "A")
|
||||
(ebox-create :content "B")
|
||||
(ebox-create :content "C")
|
||||
(ebox-create :content "D")))))
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")
|
||||
(ebox-test-box :content "C")
|
||||
(ebox-test-box :content "D")))))
|
||||
(should (string-match-p "A.*B" plain))
|
||||
(should (string-match-p "C.*D" plain))))
|
||||
|
||||
(ert-deftest ebox-grid-supports-fractional-columns ()
|
||||
"Fractional tracks should fill a definite grid width."
|
||||
(let* ((node (ebox-grid
|
||||
(let* ((node (ebox-test-grid
|
||||
:width '(120)
|
||||
:grid-template-columns '(1fr 2fr)
|
||||
(ebox-create :content "A")
|
||||
(ebox-create :content "B")))
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")))
|
||||
(lines (ebox-string-lines (ebox-render node))))
|
||||
(should (= (length lines) 1))
|
||||
(should (= (ebox--string-pixel-width (car lines)) 120))))
|
||||
|
||||
(ert-deftest ebox-grid-repeated-render-reuses-construction-time-config ()
|
||||
"Grid render and resize paths should not reparse canonical track config."
|
||||
(let* ((node (ebox-grid :width '(120)
|
||||
(let* ((node (ebox-test-grid :width '(120)
|
||||
:grid-template-columns '(1fr 2fr)
|
||||
(ebox-create :content "A")
|
||||
(ebox-create :content "B")))
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")))
|
||||
(calls 0)
|
||||
(original (symbol-function 'ebox-grid--normalize-config-props)))
|
||||
(cl-letf (((symbol-function 'ebox-grid--normalize-config-props)
|
||||
@ -56,19 +56,19 @@
|
||||
(ert-deftest ebox-grid-minmax-fractional-columns-fill-and-shrink ()
|
||||
"Zero-minimum fractional maxima should remain responsive to width."
|
||||
(dolist (width '(120 60))
|
||||
(let* ((node (ebox-grid
|
||||
(let* ((node (ebox-test-grid
|
||||
:width (list width)
|
||||
:grid-template-columns
|
||||
'((minmax (0) (fr 1)) (minmax (0) (fr 2)))
|
||||
(ebox-create :content "A" :width 'stretch)
|
||||
(ebox-create :content "B" :width 'stretch)))
|
||||
(ebox-test-box :content "A" :width 'stretch)
|
||||
(ebox-test-box :content "B" :width 'stretch)))
|
||||
(lines (ebox-string-lines (ebox-render node))))
|
||||
(should (= (ebox--string-max-pixel-width
|
||||
(car lines)) width)))))
|
||||
|
||||
(ert-deftest ebox-grid-stretch-children-fill-fractional-tracks ()
|
||||
"Stretch-width children must reflow to their assigned fractional tracks."
|
||||
(let* ((left (ebox-create :content "left" :width 'stretch))
|
||||
(let* ((left (ebox-test-box :content "left" :width 'stretch))
|
||||
(calls 0)
|
||||
(entry-left (list :node left :column 1 :column-span 1))
|
||||
(rendered (make-hash-table :test #'eq)))
|
||||
@ -85,36 +85,36 @@
|
||||
(ert-deftest ebox-grid-constrains-auto-width-children-to-track-size ()
|
||||
"Auto-width grid children should render within their assigned track."
|
||||
(let* ((ebox-viewport-width 120)
|
||||
(node (ebox-grid
|
||||
(node (ebox-test-grid
|
||||
:width '(120)
|
||||
:grid-template-columns '((50) (50))
|
||||
:gap '(0 (10))
|
||||
:border "#334155"
|
||||
(ebox-create :content "A\nA-long" :padding '(0 (4)))
|
||||
(ebox-create :content "B\nB-long" :padding '(0 (4)))))
|
||||
(ebox-test-box :content "A\nA-long" :padding '(0 (4)))
|
||||
(ebox-test-box :content "B\nB-long" :padding '(0 (4)))))
|
||||
(lines (ebox-string-lines (ebox-render node)))
|
||||
(widths (mapcar #'ebox--string-pixel-width lines)))
|
||||
(should (equal widths (make-list (length widths) 120)))))
|
||||
|
||||
(ert-deftest ebox-grid-composite-auto-row-respects-track-width ()
|
||||
"A composite auto-width row must not duplicate its Grid track viewport."
|
||||
(let* ((node (ebox-grid
|
||||
(let* ((node (ebox-test-grid
|
||||
:width '(120)
|
||||
:grid-template-columns '((120))
|
||||
(ebox-row
|
||||
(ebox-create :content "Left" :border t)
|
||||
(ebox-create :content "Right" :border t))))
|
||||
(ebox-test-row
|
||||
(ebox-test-box :content "Left" :border t)
|
||||
(ebox-test-box :content "Right" :border t))))
|
||||
(widths (mapcar #'ebox--string-pixel-width
|
||||
(ebox-string-lines (ebox-render node)))))
|
||||
(should (equal widths '(120)))))
|
||||
|
||||
(ert-deftest ebox-grid-sizes-auto-row-after-assigned-width-wrap ()
|
||||
"Auto rows should use the height of width-constrained child content."
|
||||
(let* ((node (ebox-grid
|
||||
(let* ((node (ebox-test-grid
|
||||
:width '(40)
|
||||
:grid-template-columns '((38))
|
||||
:border "#334155"
|
||||
(ebox-create
|
||||
(ebox-test-box
|
||||
:content "WIDE CONTENT WIDE CONTENT WIDE CONTENT WIDE CONTENT"
|
||||
:wrap-mode 'word)))
|
||||
(lines (ebox-string-lines (ebox-render node)))
|
||||
@ -122,35 +122,57 @@
|
||||
(should (= (length lines) 2))
|
||||
(should (equal widths (make-list (length widths) 40)))))
|
||||
|
||||
(ert-deftest ebox-grid-sizes-column-row-after-assigned-width-wrap ()
|
||||
"A composite Column row should use its final track-width text height."
|
||||
(let* ((node
|
||||
(ebox-test-grid
|
||||
:width '(20)
|
||||
:grid-template-columns '((20))
|
||||
:grid-auto-rows 'auto
|
||||
(ebox-test-column
|
||||
:width '(20)
|
||||
(ebox-test-box
|
||||
:content "fractional symbols\n1fr distributes by weight"
|
||||
:padding '(0 (2)) :border "#B85C3C"
|
||||
:wrap-mode 'word)
|
||||
(ebox-test-box :content "body" :height 1))))
|
||||
(lines (ebox-string-lines (ebox-render node)))
|
||||
(plain (mapcar #'substring-no-properties lines)))
|
||||
(should (> (length lines) 3))
|
||||
(should (equal (mapcar #'ebox--string-pixel-width lines)
|
||||
(make-list (length lines) 20)))
|
||||
(should (cl-some (lambda (line) (string-match-p "weight" line)) plain))
|
||||
(should (string-match-p "body" (car (last plain))))))
|
||||
|
||||
(ert-deftest ebox-grid-aligns-every-line-before-border-render ()
|
||||
"Grid wrapper borders should stay on one right edge across child lines."
|
||||
(let* ((node (ebox-grid
|
||||
(let* ((node (ebox-test-grid
|
||||
:width '(120)
|
||||
:grid-template-columns '((30) auto 1fr)
|
||||
:grid-template-rows '(2)
|
||||
:gap '(1 (4))
|
||||
:justify-content 'space-between
|
||||
:border "#334155"
|
||||
(ebox-create :content "FIXED\n30 PX")
|
||||
(ebox-create :content "AUTO\nINTRINSIC")
|
||||
(ebox-create :content "1FR\nREMAINDER")))
|
||||
(ebox-test-box :content "FIXED\n30 PX")
|
||||
(ebox-test-box :content "AUTO\nINTRINSIC")
|
||||
(ebox-test-box :content "1FR\nREMAINDER")))
|
||||
(lines (ebox-string-lines (ebox-render node)))
|
||||
(widths (mapcar #'ebox--string-pixel-width lines)))
|
||||
(should (equal widths (make-list (length widths) 120)))))
|
||||
|
||||
(ert-deftest ebox-grid-keeps-padded-items-inside-column-width ()
|
||||
"Stack padding should use the widest Grid line instead of its first line."
|
||||
(let* ((header (ebox-create :content "Header" :width '(720)))
|
||||
(grid (ebox-grid
|
||||
(let* ((header (ebox-test-box :content "Header" :width '(720)))
|
||||
(grid (ebox-test-grid
|
||||
:width '(718)
|
||||
:grid-template-columns '((220) 1fr)
|
||||
:gap '(1 (12))
|
||||
:border "#C97252"
|
||||
(ebox-create :content "Fixed" :width '(196)
|
||||
(ebox-test-box :content "Fixed" :width '(196)
|
||||
:padding '(1 (12)))
|
||||
(ebox-create :content "Fractional" :width '(460)
|
||||
(ebox-test-box :content "Fractional" :width '(460)
|
||||
:padding '(1 (12)))))
|
||||
(lines (ebox-string-lines (ebox-render (ebox-column header grid))))
|
||||
(lines (ebox-string-lines (ebox-render (ebox-test-column header grid))))
|
||||
(widths (mapcar #'ebox--string-pixel-width lines)))
|
||||
(should (= (apply #'max widths) 720))))
|
||||
|
||||
@ -158,23 +180,23 @@
|
||||
"Explicit placement should support a cell spanning two columns."
|
||||
(let ((plain
|
||||
(ebox-grid-test--plain
|
||||
(ebox-grid
|
||||
(ebox-test-grid
|
||||
:grid-template-columns '((20) (20))
|
||||
:grid-template-rows '(1 1)
|
||||
(ebox-create :content "Header" :grid-column '(1 :span 2))
|
||||
(ebox-create :content "Left" :grid-row 2 :grid-column 1)
|
||||
(ebox-create :content "Right" :grid-row 2 :grid-column 2)))))
|
||||
(ebox-test-box :content "Header" :grid-column '(1 :span 2))
|
||||
(ebox-test-box :content "Left" :grid-row 2 :grid-column 1)
|
||||
(ebox-test-box :content "Right" :grid-row 2 :grid-column 2)))))
|
||||
(should (string-match-p "Header" plain))
|
||||
(should (string-match-p "Left.*Right" plain))))
|
||||
|
||||
(ert-deftest ebox-grid-uses-implicit-track-templates ()
|
||||
"Implicit columns should use `:grid-auto-columns' when they grow."
|
||||
(let* ((node (ebox-grid
|
||||
(let* ((node (ebox-test-grid
|
||||
:grid-template-columns '((20))
|
||||
:grid-auto-columns '((30))
|
||||
:grid-auto-flow 'column
|
||||
(ebox-create :content "A")
|
||||
(ebox-create :content "B")))
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")))
|
||||
(line (car (ebox-string-lines (ebox-render node)))))
|
||||
(should (= (ebox--string-pixel-width line) 50))))
|
||||
|
||||
@ -182,25 +204,25 @@
|
||||
"Implicit rows should retain the declared template column count."
|
||||
(let ((plain
|
||||
(ebox-grid-test--plain
|
||||
(ebox-grid
|
||||
(ebox-test-grid
|
||||
:grid-template-columns '((20) (20) (20))
|
||||
(ebox-create :content "A")
|
||||
(ebox-create :content "B")
|
||||
(ebox-create :content "C")
|
||||
(ebox-create :content "D")
|
||||
(ebox-create :content "E")
|
||||
(ebox-create :content "F")))))
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")
|
||||
(ebox-test-box :content "C")
|
||||
(ebox-test-box :content "D")
|
||||
(ebox-test-box :content "E")
|
||||
(ebox-test-box :content "F")))))
|
||||
(dolist (letter '("A" "B" "C" "D" "E" "F"))
|
||||
(should (string-match-p letter plain)))))
|
||||
|
||||
(ert-deftest ebox-grid-supports-repeat-minmax-and-content-alignment ()
|
||||
"Grid should normalize repeat/minmax tracks and align fixed tracks."
|
||||
(let* ((node (ebox-grid
|
||||
(let* ((node (ebox-test-grid
|
||||
:width '(100)
|
||||
:justify-content 'center
|
||||
:grid-template-columns '((minmax (20) (30)) (repeat 1 (20)))
|
||||
(ebox-create :content "A")
|
||||
(ebox-create :content "B")))
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")))
|
||||
(line (car (ebox-string-lines (ebox-render node)))))
|
||||
(should (= (ebox--string-pixel-width line) 100))
|
||||
(should (= (length (seq-filter (lambda (char) (= char ?\s)) line))
|
||||
@ -210,15 +232,15 @@
|
||||
"Invalid flow and non-positive spans should be rejected."
|
||||
(should-error
|
||||
(ebox-render
|
||||
(ebox-grid :grid-auto-flow 'diagonal (ebox-create :content "A"))))
|
||||
(ebox-test-grid :grid-auto-flow 'diagonal (ebox-test-box :content "A"))))
|
||||
(should-error
|
||||
(ebox-render
|
||||
(ebox-grid
|
||||
(ebox-create :content "A" :grid-column-span 0))))
|
||||
(ebox-test-grid
|
||||
(ebox-test-box :content "A" :grid-column-span 0))))
|
||||
(should-error
|
||||
(ebox-render
|
||||
(ebox-grid
|
||||
(ebox-create :content "A" :grid-column 0)))))
|
||||
(ebox-test-grid
|
||||
(ebox-test-box :content "A" :grid-column 0)))))
|
||||
|
||||
(ert-deftest ebox-grid-rejects-invalid-ecss-placement-before-layout ()
|
||||
"ECSS rules should reject invalid Grid placement before layout runs."
|
||||
@ -233,10 +255,10 @@
|
||||
(should (ebox-style-add-rule ".valid" (list :grid-column placement))))))
|
||||
|
||||
(ert-deftest ebox-grid-region-update-reflows-grid-child ()
|
||||
"Grid children should retain region updates through buffer rendering."
|
||||
(let* ((left (ebox-create :content "A" :id "left" :width 20))
|
||||
(right (ebox-create :content "B" :id "right" :width 20))
|
||||
(node (ebox-grid :grid-template-columns '((20) (20)) left right))
|
||||
"Replacing one typed Grid child should reflow and preserve its sibling."
|
||||
(let* ((left (ebox-test-box :content "A" :host-ref 'left :width 20))
|
||||
(right (ebox-test-box :content "B" :id "right" :width 20))
|
||||
(node (ebox-test-grid :grid-template-columns '((20) (20)) left right))
|
||||
(buffer (ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-grid-update*") node)))
|
||||
(unwind-protect
|
||||
@ -244,8 +266,11 @@
|
||||
(should (with-current-buffer buffer
|
||||
(and (string-match-p "A" (buffer-string))
|
||||
(string-match-p "B" (buffer-string)))))
|
||||
(ebox-region-update (ebox-region-resolve buffer "left")
|
||||
:content "Updated")
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'left
|
||||
(ebox-test-box :content "Updated" :host-ref 'left :width 20))
|
||||
(ebox-commit buffer candidate))
|
||||
(let ((updated (with-current-buffer buffer (buffer-string))))
|
||||
(should (string-match-p "Updated" updated))
|
||||
(should (string-match-p "B" updated))))
|
||||
@ -257,9 +282,10 @@
|
||||
(let ((node
|
||||
(ebox-build
|
||||
'(grid :grid-template-columns ((20) (20))
|
||||
(box :content "A")
|
||||
(box :content "B")))))
|
||||
(should (eq (plist-get node :ebox-type) 'grid))
|
||||
(box "A")
|
||||
(box "B")))))
|
||||
(should (ebox-box-node-p node))
|
||||
(should (eq (ebox-layout-config-kind (ebox-box-node-layout node)) 'grid))
|
||||
(should (string-match-p "A" (ebox-grid-test--plain node)))
|
||||
(should (string-match-p "B" (ebox-grid-test--plain node)))))
|
||||
|
||||
@ -268,9 +294,10 @@
|
||||
(let ((node
|
||||
(ebox-build
|
||||
'(grid :grid-template-columns ((20) (20))
|
||||
(box :grid-column (1 :span 2) :content "Header")))))
|
||||
(box :grid-column (1 :span 2) "Header")))))
|
||||
(should (string-match-p "Header" (ebox-grid-test--plain node)))
|
||||
(should (equal (plist-get (car (plist-get node :children)) :grid-column)
|
||||
(should (equal (ebox-style-node-specified-value
|
||||
(car (ebox-box-node-children node)) :grid-column)
|
||||
'(1 :span 2)))))
|
||||
|
||||
(provide 'ebox-grid-tests)
|
||||
|
||||
@ -162,15 +162,6 @@
|
||||
("ebox-call-with-render-burst" . "defun")
|
||||
("ebox-render" . "defun")
|
||||
("ebox-region-ids" . "defun")
|
||||
("ebox-concat" . "defun")
|
||||
("ebox-spacer" . "defun")
|
||||
("ebox-row" . "defun")
|
||||
("ebox-stack" . "defun")
|
||||
("ebox-column" . "defun")
|
||||
("ebox-flex-item" . "defun")
|
||||
("ebox-flex" . "defun")
|
||||
("ebox-grid-fr" . "defun")
|
||||
("ebox-grid" . "defun")
|
||||
("ebox-grid-layout-create" . "defun")
|
||||
("ebox-normal-layout-create" . "defun")
|
||||
("ebox-row-layout-create" . "defun")
|
||||
@ -179,7 +170,6 @@
|
||||
("ebox-text-create" . "defun")
|
||||
("ebox-box-create" . "defun")
|
||||
("ebox-build" . "defun")
|
||||
("ebox-create" . "defun")
|
||||
("ebox-scroll-down" . "defun")
|
||||
("ebox-scroll-up" . "defun")
|
||||
("ebox-region-update" . "defun")
|
||||
@ -212,22 +202,15 @@
|
||||
ebox-candidate-replace-root
|
||||
ebox-candidate-replace-host-ref
|
||||
ebox-clear-cache
|
||||
ebox-column
|
||||
ebox-normal-layout-create
|
||||
ebox-row-layout-create
|
||||
ebox-column-layout-create
|
||||
ebox-text-create
|
||||
ebox-box-create
|
||||
ebox-commit
|
||||
ebox-concat
|
||||
ebox-create
|
||||
ebox-display-buffer
|
||||
ebox-display-signature
|
||||
ebox-flex
|
||||
ebox-flex-layout-create
|
||||
ebox-flex-item
|
||||
ebox-grid
|
||||
ebox-grid-fr
|
||||
ebox-grid-layout-create
|
||||
ebox-host-ref-bounds
|
||||
ebox-host-ref-position
|
||||
@ -241,7 +224,6 @@
|
||||
ebox-render-burst-end
|
||||
ebox-render-to-buffer
|
||||
ebox-rerender-buffer-with-context
|
||||
ebox-row
|
||||
ebox-scroll-down
|
||||
ebox-scroll-page-down
|
||||
ebox-scroll-page-up
|
||||
@ -252,11 +234,17 @@
|
||||
ebox-selector-query-all
|
||||
ebox-selector-query-buffer
|
||||
ebox-selector-update-buffer
|
||||
ebox-spacer
|
||||
ebox-stack
|
||||
ebox-string-pixel-width))
|
||||
(should (memq entry ebox-public-api))))
|
||||
|
||||
(ert-deftest ebox-legacy-creation-surface-is-absent ()
|
||||
"Only typed constructors and `ebox-build' may create public nodes."
|
||||
(dolist (entry '(ebox-test-box ebox-concat ebox-stack ebox-spacer
|
||||
ebox-row ebox-column ebox-flex ebox-flex-item
|
||||
ebox-grid ebox-grid-fr))
|
||||
(should-not (fboundp entry))
|
||||
(should-not (memq entry ebox-public-api))))
|
||||
|
||||
(ert-deftest ebox-live-buffer-publication-uses-only-tp-surfaces ()
|
||||
"The facade should expose no raw live-buffer writer beside TP surfaces."
|
||||
(require 'ebox)
|
||||
|
||||
@ -21,22 +21,22 @@
|
||||
(defun ebox-selector-test--width-unit-form (width)
|
||||
"Return a layout whose root WIDTH constrains nested auto layout."
|
||||
`(column :id root :width ,width
|
||||
(box :content "Header text wraps when the root is narrow"
|
||||
:padding (0 (12)) :border "#111111")
|
||||
(box :padding (0 (12)) :border "#111111"
|
||||
"Header text wraps when the root is narrow")
|
||||
(flex :flex-flow (row wrap) :column-gap (12)
|
||||
:padding (0 (12)) :border "#D97757"
|
||||
(column
|
||||
(box :content "row" :width (220) :border "#D97757")
|
||||
(box :width (220) :border "#D97757" "row")
|
||||
(flex :width (220) :height 4 :border "#D97757"
|
||||
(item (box :content "A" :width (42)))
|
||||
(item (box :content "B" :width (42)))
|
||||
(item (box :content "C" :width (42)))))
|
||||
(box :width (42) "A")
|
||||
(box :width (42) "B")
|
||||
(box :width (42) "C")))
|
||||
(column
|
||||
(box :content "row-reverse" :width (220) :border "#D97757")
|
||||
(box :width (220) :border "#D97757" "row-reverse")
|
||||
(flex :width (220) :height 4 :border "#D97757"
|
||||
(item (box :content "A" :width (42)))
|
||||
(item (box :content "B" :width (42)))
|
||||
(item (box :content "C" :width (42))))))))
|
||||
(box :width (42) "A")
|
||||
(box :width (42) "B")
|
||||
(box :width (42) "C"))))))
|
||||
|
||||
(defun ebox-selector-test--rendered-widths (rendered)
|
||||
"Return pixel widths for RENDERED lines."
|
||||
@ -69,8 +69,8 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let* ((node (ebox-build
|
||||
'(column :id root :class page
|
||||
(box :id summary :class (card warning) :content "A")
|
||||
(box :class card :content "B"))))
|
||||
(box :id summary :class (card warning) "A")
|
||||
(box :class card "B"))))
|
||||
(matches (ebox-selector-query-all node ".warning")))
|
||||
(should (= (length matches) 1))
|
||||
(should (equal (plist-get (car matches) :region-id)
|
||||
@ -83,8 +83,8 @@
|
||||
(let* ((node (ebox-build
|
||||
'(column :class page
|
||||
(row :class toolbar
|
||||
(box :class action :content "Run"))
|
||||
(box :class action :content "Loose")))))
|
||||
(box :class action "Run"))
|
||||
(box :class action "Loose")))))
|
||||
(should (= (length (ebox-selector-query-all node ".toolbar > box")) 1))
|
||||
(should (= (length (ebox-selector-query-all node ".page .action")) 2))))
|
||||
|
||||
@ -93,9 +93,9 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let ((node (ebox-build
|
||||
'(column
|
||||
(box :id first :content "A")
|
||||
(box :id second :class later :content "B")
|
||||
(box :id third :class later :content "C")))))
|
||||
(box :id first "A")
|
||||
(box :id second :class later "B")
|
||||
(box :id third :class later "C")))))
|
||||
(should (equal
|
||||
(mapcar (lambda (match)
|
||||
(ebox-tree-node-id (plist-get match :node)))
|
||||
@ -112,8 +112,8 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let ((node (ebox-build
|
||||
'(column
|
||||
(box :class card :content "A")
|
||||
(box :content "B"))))
|
||||
(box :class card "A")
|
||||
(box "B"))))
|
||||
(calls 0)
|
||||
(matcher (symbol-function 'ecss-selector-match-p)))
|
||||
(cl-letf (((symbol-function 'ecss-selector-match-p)
|
||||
@ -144,9 +144,9 @@
|
||||
|
||||
(ert-deftest ebox-selector-queries-explicit-attributes ()
|
||||
"Attribute queries should read only explicit selector metadata."
|
||||
(let ((node (ebox-build
|
||||
'(box :content "Run"
|
||||
:selector-attributes '((role . button))))))
|
||||
(let ((node (ebox-test-box
|
||||
:content "Run"
|
||||
:selector-attributes '((role . button)))))
|
||||
(should (= (length (ebox-selector-query-all node "[role=button]")) 1))
|
||||
(should-not (ebox-selector-query-all node "[content=Run]"))))
|
||||
|
||||
@ -155,8 +155,8 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let* ((node (ebox-build
|
||||
'(column
|
||||
(box :id summary :class card :content "Summary")
|
||||
(box :class card :content "Other"))))
|
||||
(box :id summary :class card "Summary")
|
||||
(box :class card "Other"))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
@ -178,8 +178,8 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let* ((node (ebox-build
|
||||
'(column
|
||||
(box :id summary :class card :content "Summary")
|
||||
(box :class card :content "Other"))))
|
||||
(box :id summary :class card "Summary")
|
||||
(box :class card "Other"))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
@ -200,8 +200,8 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let* ((node (ebox-build
|
||||
'(column
|
||||
(box :id summary :class (card warning) :content "Summary")
|
||||
(box :class card :content "Other"))))
|
||||
(box :id summary :class (card warning) "Summary")
|
||||
(box :class card "Other"))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
@ -225,8 +225,8 @@
|
||||
(let* ((node (ebox-build
|
||||
'(column :class page
|
||||
(row :class toolbar
|
||||
(box :class action :content "Run"))
|
||||
(box :class action :content "Loose"))))
|
||||
(box :class action "Run"))
|
||||
(box :class action "Loose"))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
@ -249,8 +249,8 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let* ((node (ebox-build
|
||||
'(column :id shell
|
||||
(box :class target :content "A")
|
||||
(box :content "B"))))
|
||||
(box :class target "A")
|
||||
(box "B"))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
@ -262,9 +262,13 @@
|
||||
(should-not
|
||||
(gethash 'column (plist-get state :selector-type-table)))
|
||||
(should (= (gethash
|
||||
'column
|
||||
'box
|
||||
(plist-get state :runtime-type-count-table))
|
||||
1)))
|
||||
3))
|
||||
(should (= (gethash
|
||||
'text
|
||||
(plist-get state :runtime-type-count-table))
|
||||
2)))
|
||||
(should-not (ebox-selector-query-buffer
|
||||
buffer "column .target")))
|
||||
(when (and buffer (buffer-live-p buffer))
|
||||
@ -276,8 +280,8 @@
|
||||
(let* ((node (ebox-build
|
||||
'(column
|
||||
(box :id card
|
||||
:content "Text wraps when narrow"
|
||||
:wrap-mode word))))
|
||||
:wrap-mode word
|
||||
"Text wraps when narrow"))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
@ -299,7 +303,7 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let* ((node (ebox-build
|
||||
'(column
|
||||
(box :id card :content "Card" :border "#111111"))))
|
||||
(box :id card :border "#111111" "Card"))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
@ -321,7 +325,7 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let* ((node (ebox-build
|
||||
'(column
|
||||
(box :id card :content "Card"))))
|
||||
(box :id card "Card"))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
@ -371,10 +375,10 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let* ((target-node (ebox-build
|
||||
'(column
|
||||
(box :id target-card :content "Target"))))
|
||||
(box :id target-card "Target"))))
|
||||
(other-node (ebox-build
|
||||
'(column
|
||||
(box :id other-card :content "Other"))))
|
||||
(box :id other-card "Other"))))
|
||||
target-buffer other-buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
@ -416,7 +420,7 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let* ((node (ebox-build
|
||||
'(column
|
||||
(box :id card :content "Card"))))
|
||||
(box :id card "Card"))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
@ -449,8 +453,8 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let* ((node (ebox-build
|
||||
'(column
|
||||
(box :class card :content "One")
|
||||
(box :class card :content "Two"))))
|
||||
(box :class card "One")
|
||||
(box :class card "Two"))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
@ -485,8 +489,8 @@
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let* ((node (ebox-build
|
||||
'(column
|
||||
(box :class card :content "One")
|
||||
(box :class card :content "Two"))))
|
||||
(box :class card "One")
|
||||
(box :class card "Two"))))
|
||||
buffer)
|
||||
(unwind-protect
|
||||
(progn
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user