feat: retain Row and Column within canonical Box identity

This commit is contained in:
Kinneyzhang 2026-08-26 19:03:35 +08:00
parent b530b0513c
commit 53001719c4
12 changed files with 301 additions and 111 deletions

View File

@ -587,7 +587,7 @@ style-rule functions immediately below are public module-level style APIs.
| Function | Use |
| --- | --- |
| `ebox-text-create`, `ebox-normal-layout-create`, `ebox-box-create` | Framework-integration port for typed TextNode, Normal LayoutConfig, and BoxNode values. |
| `ebox-text-create`, `ebox-normal-layout-create`, `ebox-row-layout-create`, `ebox-column-layout-create`, `ebox-box-create` | Framework-integration port for typed TextNode, Normal/Row/Column LayoutConfig, and BoxNode values. |
| `ebox-create`, `ebox-build` | Create a legacy node or compile the `.ebox` list DSL during migration. |
| `ebox-concat`, `ebox-stack`, `ebox-row`, `ebox-column`, `ebox-spacer` | Compose simple horizontal/vertical layouts and blank boxes. |
| `ebox-flex`, `ebox-flex-item` | Build flex containers and item metadata. |

View File

@ -533,7 +533,7 @@ style API。
| 函数 | 用途 |
| --- | --- |
| `ebox-text-create`、`ebox-normal-layout-create`、`ebox-box-create` | 框架集成使用的 typed TextNode、Normal LayoutConfig 与 BoxNode 端口。 |
| `ebox-text-create`、`ebox-normal-layout-create`、`ebox-row-layout-create`、`ebox-column-layout-create`、`ebox-box-create` | 框架集成使用的 typed TextNode、Normal/Row/Column LayoutConfig 与 BoxNode 端口。 |
| `ebox-create`、`ebox-build` | 迁移期间创建旧节点或编译 `.ebox` list DSL。 |
| `ebox-concat`、`ebox-stack`、`ebox-row`、`ebox-column`、`ebox-spacer` | 组合简单横向/纵向布局与空白 box。 |
| `ebox-flex`、`ebox-flex-item` | 构建 Flex 容器与 item metadata。 |

View File

@ -73,8 +73,8 @@
"Return non-nil when PROPERTY belongs to canonical Text measurement."
(eq (plist-get property :group) 'typography))
(defun ebox-canonical--normal-box-property-p (property)
"Return non-nil when PROPERTY belongs to Normal Box geometry/participation."
(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)))
@ -96,6 +96,16 @@
"Return the canonical Normal layout config."
(ebox-layout-config--create :kind 'normal :props nil))
;;;###autoload
(defun ebox-row-layout-create ()
"Return the canonical Row layout config."
(ebox-layout-config--create :kind 'row :props nil))
;;;###autoload
(defun ebox-column-layout-create ()
"Return the canonical Column layout config."
(ebox-layout-config--create :kind 'column :props nil))
(defun ebox-node-kind (node)
"Return canonical NODE kind, or nil for a legacy runtime node."
(and (listp node) (plist-get node :ebox-kind)))
@ -128,7 +138,7 @@
"Return canonical BoxNode NODE's retained canonical children."
(unless (ebox-box-node-p node)
(error "Expected canonical BoxNode, got %S" node))
(plist-get node :ebox-canonical-children))
(plist-get node :children))
;;;###autoload
(defun ebox-text-create (&rest plist)
@ -159,9 +169,8 @@
"Create a canonical BoxNode from evaluated PLIST.
`:layout' must occur exactly once as an `ebox-layout-config'.
`:children' is a list of canonical nodes. This first vertical slice supports
Normal layout with zero or one child; later slices extend the same typed port
to the other Layout variants."
`:children' is a list of canonical nodes. Normal layout accepts zero or one
child. Row and Column layouts accept any number of children."
(ebox-canonical--validate-plist plist "ebox-box-create")
(let* ((layout (ebox-canonical--required-field
plist :layout "Ebox Box"))
@ -189,23 +198,30 @@ to the other Layout variants."
children)
(error "Ebox Box children must be canonical Text/Box nodes: %S"
children))
(unless (eq (ebox-layout-config-kind layout) 'normal)
(error "Ebox Box Layout is not implemented in this slice: %S"
(unless (memq (ebox-layout-config-kind layout) '(normal row column))
(error "Ebox Box Layout is not implemented: %S"
(ebox-layout-config-kind layout)))
(when (> (length children) 1)
(unless (null (ebox-layout-config-props layout))
(error "Ebox Box Layout properties are not implemented: %S"
(ebox-layout-config-props layout)))
(when (and (eq (ebox-layout-config-kind layout) 'normal)
(> (length children) 1))
(error "Ebox Box Normal layout currently accepts at most one child"))
(ebox-canonical--assert-property-role
props #'ebox-canonical--normal-box-property-p "Ebox Box")
(let ((node
(apply #'ebox-create
(append props
(when children
(list :ebox-content-node (car children)))))))
props #'ebox-canonical--box-frame-property-p "Ebox Box")
(let ((node (apply #'ebox-create props)))
(plist-put node :ebox-kind 'box)
(plist-put node :ebox-layout-config layout)
(plist-put node :ebox-canonical-children children)
(plist-put node :children children)
(plist-put node :ebox-source-handle source-handle)
(plist-put node :display (list outer 'flow))
;; 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)
(plist-put node :display
(list outer
(pcase (ebox-layout-config-kind layout)
('normal 'flow)
(kind kind))))
node)))
(provide 'ebox-canonical)

View File

@ -570,8 +570,8 @@ grapheme."
('box
(when-let* ((region-id (ebox-get node :region-id)))
(puthash region-id node ebox--region-box-table))
(ebox--flex-recache-source-boxes
(plist-get node :ebox-content-node)))
(dolist (child (ebox-tree-node-children node))
(ebox--flex-recache-source-boxes child)))
('concat
(dolist (child (ebox--layout-children node))
(ebox--flex-recache-source-boxes child)))

View File

@ -7744,7 +7744,7 @@ the surface is allowed to reuse retained TP objects."
"Return non-nil when path-copied NODE keeps the published line count."
(and (eq (plist-get node :ebox-type) 'box)
(numberp (ebox-get node :width))
(null (plist-get node :ebox-content-node))
(null (ebox-tree-node-children node))
(= (length spans)
(with-current-buffer buffer
(length (ebox-string-lines (ebox--format-content node)))))))

View File

@ -17,6 +17,7 @@
(declare-function ebox--render-grid "ebox-grid" (node))
(declare-function ebox--render-flex "ebox-flex" (node))
(declare-function ebox-layout-config-kind "ebox-canonical" (config))
(declare-function ebox--render-with-cache
"ebox-incremental" (node &optional force cache-probe))
(declare-function ebox-create "ebox" (&rest plist))
@ -236,6 +237,7 @@ and its formatted content exactly fills the used height."
(when (and (not ebox--flat-preformatted-box-fast-path-disabled)
(not (eq (ebox-get box :visibility) 'hidden))
(null (ebox--box-content-node box))
(not (plist-member box :children))
(stringp (ebox-get box :content))
(eq (ebox-get box :vertical-align) 'top)
(equal (or (ebox-get box :min-height) 0) 0)
@ -883,6 +885,11 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
"Return BOX's lazy child layout node, if any."
(plist-get box :ebox-content-node))
(defun ebox--box-layout-kind (box)
"Return canonical BOX's typed layout kind, or nil for a legacy box."
(when-let* ((config (plist-get box :ebox-layout-config)))
(ebox-layout-config-kind config)))
(defun ebox--box-content-cache-context ()
"Return the current render context for lazy box content caching."
(list ebox-viewport-width
@ -891,43 +898,79 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
ebox--inline-auto-width-intrinsic-p
ebox--render-region-id))
(defun ebox--render-box-content-node (box node)
"Render NODE as BOX content, using the render-pass cache when available."
(let* ((content-viewport (ebox--wrapper-content-viewport-pixel box))
(content
(defun ebox--render-box-content-cached (box renderer)
"Return BOX content from RENDERER, reusing the render-pass cache."
(if (null ebox--box-content-render-cache)
(ebox--render-node-as-box-content node box)
(funcall renderer)
(let* ((context (ebox--box-content-cache-context))
(entries (gethash box ebox--box-content-render-cache))
(cached (assoc context entries)))
(if cached
(cdr cached)
(let ((rendered
(ebox--render-node-as-box-content node box)))
(let ((rendered (funcall renderer)))
(puthash box
(cons (cons context rendered) entries)
ebox--box-content-render-cache)
rendered))))))
rendered)))))
(defun ebox--record-box-content-width-exact (box content composite-p)
"Record exact width for BOX CONTENT when COMPOSITE-P is non-nil."
(let ((content-viewport (ebox--wrapper-content-viewport-pixel box)))
(when (and content-viewport
composite-p
(not ebox--intrinsic-layout-measurement)
(equal (gethash content ebox--rendered-uniform-width-table)
content-viewport))
(plist-put box :ebox-content-width-exact-p t))))
(defun ebox--render-box-content-node (box node)
"Render NODE as BOX content, using the render-pass cache when available."
(let ((content
(ebox--render-box-content-cached
box (lambda () (ebox--render-node-as-box-content node box)))))
;; A child layout has already resolved wrapping, alignment, and overflow
;; against CONTENT-VIEWPORT. Preserve that fact on the owning box so the
;; generic string formatter does not traverse the same propertized output
;; a second time. Intrinsic measurement is deliberately excluded: it
;; produces a natural-width probe, not final box content.
(when (and content-viewport
(not (eq (and (listp node) (plist-get node :ebox-type))
'box))
(not ebox--intrinsic-layout-measurement)
(equal (gethash content ebox--rendered-uniform-width-table)
content-viewport))
(plist-put box :ebox-content-width-exact-p t))
(ebox--record-box-content-width-exact
box content
(not (eq (and (listp node) (plist-get node :ebox-type)) 'box)))
content))
(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))
(content
(ebox--render-box-content-cached
box
(lambda ()
(pcase kind
('normal
(if children
(ebox--render-node-as-box-content (car children) box)
""))
('row
(ebox--call-with-box-content-context
box (lambda () (ebox--render-row-children children))))
('column
(ebox--call-with-box-content-context
box (lambda () (ebox--render-column-children children))))
(_ (error "Ebox Box has unsupported LayoutConfig: %S" kind)))))))
(ebox--record-box-content-width-exact
box content (memq kind '(row column)))
content))
(defun ebox--box-content (box)
"Return BOX content, rendering any lazy child layout content if present."
(ebox--propertize-typography
(or (when-let* ((node (ebox--box-content-node box)))
(ebox--render-box-content-node box node))
(ebox-get box :content))
(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))
(defun ebox--line-min-content-pixel (line)
@ -1039,6 +1082,21 @@ property parsers."
(min (or max-width 999999999)
(max 0 preferred-width))))))
(defun ebox--call-with-box-content-context (props function)
"Call FUNCTION in the content viewport established by box PROPS."
(let ((content-viewport (ebox--wrapper-content-viewport-pixel props)))
(cond
((eq (ebox-get props :width) 'max-content)
(let ((ebox-viewport-width nil)
(ebox--intrinsic-layout-measurement t)
(ebox--inline-auto-width-intrinsic-p nil))
(funcall function)))
(content-viewport
(let ((ebox-viewport-width content-viewport)
(ebox--inline-auto-width-intrinsic-p nil))
(funcall function)))
(t (funcall function)))))
(defun ebox--render-node-as-box-content (node props)
"Render NODE as preformatted content for a box with PROPS.
When PROPS establish a content-box viewport, nested auto and `(viewport)'
@ -1046,19 +1104,11 @@ widths resolve against that inner content box instead of the outer caller
viewport. A max-content wrapper instead measures the composite NODE without
an inherited viewport."
(let ((content-viewport (ebox--wrapper-content-viewport-pixel props)))
(cond
((eq (ebox-get props :width) 'max-content)
(let ((ebox-viewport-width nil)
(ebox--intrinsic-layout-measurement t)
(ebox--inline-auto-width-intrinsic-p nil))
(ebox-render node)))
(content-viewport
(let ((ebox-viewport-width content-viewport)
;; A definite wrapper establishes a new containing block. Its
;; child layout must resume normal cross-axis stretch semantics,
;; even when the wrapper itself is an item of an intrinsic row.
(ebox--inline-auto-width-intrinsic-p nil))
(if (and (hash-table-p ebox--render-cache-table)
(ebox--call-with-box-content-context
props
(lambda ()
(if (and content-viewport
(hash-table-p ebox--render-cache-table)
(not ebox--intrinsic-layout-measurement)
(null ebox--render-region-id)
(fboundp 'ebox--render-cache-probe)
@ -1077,9 +1127,7 @@ an inherited viewport."
:scroll-lookahead
ebox--scroll-window-initial-lookahead-lines-override
:scroll-disabled ebox--scroll-window-render-disabled)))
(ebox-render node))))
(t
(ebox-render node)))))
(ebox-render node))))))
(defun ebox--resolve-size-content-pixel (box value fallback)
"Resolve a horizontal size VALUE to BOX content pixels.
@ -1702,8 +1750,8 @@ A sole child Range keeps a material row parent so the Range stays addressable."
:display '(block row)
:children nodes))))
(defun ebox--render-concat (node)
"Render a concat NODE to a string (internal, called by `ebox-render').
(defun ebox--render-row-children (children)
"Render flat row CHILDREN to one multi-line string.
Auto-width children are rendered at intrinsic inline size while explicit
viewport/definite widths keep their containing-block semantics. Shorter
children are padded with blank lines so all reach the same height before
@ -1716,7 +1764,7 @@ horizontal concatenation."
;; the ambient containing block.
(let ((ebox--inline-auto-width-intrinsic-p t))
(ebox--render-with-cache child)))
(ebox--layout-children node)))
children))
(max-h (if rendered
(apply #'max (mapcar #'ebox-string-height rendered))
0))
@ -1729,6 +1777,10 @@ horizontal concatenation."
(ebox-lines-join (apply #'cl-mapcar #'concat line-lists))
"")))
(defun ebox--render-concat (node)
"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.
@ -3074,8 +3126,8 @@ only used for incomplete lazy prefixes."
formatted-lines
(seq-take formatted-lines target-lines))))))))))
(defun ebox--render-stack (node)
"Render a stack NODE to a string (internal, called by `ebox-render').
(defun ebox--render-column-children (children)
"Render flat column CHILDREN to one multi-line string.
Stack leaves are rendered independently. When a containing-block viewport is
bound, narrower rows are padded to that available width, while wider definite
@ -3089,7 +3141,7 @@ ambient horizontal viewport."
(let ((rendered (ebox--render-with-cache leaf)))
(cons rendered
(ebox--string-max-pixel-width rendered))))
(ebox--stack-leaves node)))
children))
(max-w (if rendered-items
(apply #'max (mapcar #'cdr rendered-items))
0))
@ -3115,6 +3167,10 @@ ambient horizontal viewport."
(ebox--record-rendered-uniform-width rendered target-w)
rendered))))
(defun ebox--render-stack (node)
"Render a legacy stack NODE to a string."
(ebox--render-column-children (ebox--stack-leaves node)))
(provide 'ebox-layout)
;;; ebox-layout.el ends here

View File

@ -1621,6 +1621,8 @@ Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
(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))
(child (if (or child-override-p child-resolved-p)
child-override
@ -1910,8 +1912,9 @@ 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)))
(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)))

View File

@ -242,7 +242,7 @@ Scoped rules remain tree-dependent because scope distance reads ancestors."
(defun ebox-surface--style-consuming-node-p (node)
"Return non-nil when NODE owns a paintable style surface."
(and (eq (plist-get node :ebox-type) 'box)
(null (plist-get node :ebox-content-node))
(null (ebox-tree-node-children node))
(> (length (or (plist-get node :content) "")) 0)))
(defun ebox-surface--inline-inheritance-required-p (root)

View File

@ -139,7 +139,9 @@ This is an optimization for snapshot capture, not buffer runtime state.")
"Return NODE's direct child nodes without assigning runtime ids."
(pcase (and (listp node) (plist-get node :ebox-type))
('box
(delq nil (list (plist-get node :ebox-content-node))))
(if (plist-member node :children)
(plist-get node :children)
(delq nil (list (plist-get node :ebox-content-node)))))
('concat
(ebox-tree-layout-children node))
('stack
@ -246,7 +248,8 @@ still copied while every nested value remains shared."
(setq copy
(ebox-tree--replace-direct-child-property
copy property replacements)))
(when (and (memq type '(concat stack flex grid))
(when (and (or (memq type '(concat stack flex grid))
(and (eq type 'box) (plist-member copy :children)))
(plist-member copy :children))
(let* ((children (plist-get copy :children))
(replaced
@ -393,10 +396,17 @@ 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)
(plist-put shell :children
(mapcar
(lambda (child)
(ebox-tree--runtime-identity-skeleton
child node-id-set region-id-set))
(plist-get node :children)))
(when-let* ((child (plist-get node :ebox-content-node)))
(plist-put shell :ebox-content-node
(ebox-tree--runtime-identity-skeleton
child node-id-set region-id-set))))
child node-id-set region-id-set)))))
((or 'concat 'stack)
(plist-put shell :children
(mapcar
@ -809,8 +819,10 @@ candidate runtime index agree."
"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)
(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
@ -924,9 +936,11 @@ not only the public update ids."
(let ((region-ids
(pcase (plist-get node :ebox-type)
('box
(cons (ebox--ensure-region-id node)
(ebox-tree-node-all-region-ids
(plist-get node :ebox-content-node))))
(cons
(ebox--ensure-region-id node)
(apply #'append
(mapcar #'ebox-tree-node-all-region-ids
(ebox-tree-node-children node)))))
('concat
(apply #'append
(mapcar #'ebox-tree-node-all-region-ids
@ -969,8 +983,10 @@ cannot be inspected and reports no conflict."
(pcase (plist-get node :ebox-type)
('box
(or (and inherited (plist-get node paint-key) t)
(ebox-tree-node-paint-conflict-p
(plist-get node :ebox-content-node) paint-key t)))
(cl-some
(lambda (child)
(ebox-tree-node-paint-conflict-p child paint-key t))
(ebox-tree-node-children node))))
('concat
(cl-some (lambda (child)
(ebox-tree-node-paint-conflict-p child paint-key t))
@ -1010,10 +1026,12 @@ wrapper's visible content is produced by the flex renderer."
('box
(or (when (equal (ebox-get node :region-id) region-id)
(list node))
(when-let* ((path (ebox-tree-node-path-to-region
(plist-get node :ebox-content-node)
region-id)))
(append path (list node)))))
(catch 'found
(dolist (child (ebox-tree-node-children node))
(when-let* ((path
(ebox-tree-node-path-to-region child region-id)))
(throw 'found (append path (list node)))))
nil)))
('concat
(catch 'found
(dolist (child (ebox-tree-layout-children node))
@ -1060,8 +1078,8 @@ wrapper's visible content is produced by the flex renderer."
(pcase (plist-get node :ebox-type)
('box
(or (ebox--box-visible-overflow-p node)
(ebox-tree-node-visible-overflow-p
(plist-get node :ebox-content-node))))
(cl-some #'ebox-tree-node-visible-overflow-p
(ebox-tree-node-children node))))
('concat
(cl-some #'ebox-tree-node-visible-overflow-p
(ebox-tree-layout-children node)))

11
ebox.el
View File

@ -650,8 +650,11 @@ Use this to capture ids *before* inserting into a buffer:
(let ((type (plist-get node :ebox-type)))
(cond
((eq type 'box)
(cons (ebox--ensure-region-id node)
(ebox-region-ids (plist-get node :ebox-content-node))))
(cons
(ebox--ensure-region-id node)
(apply #'append
(mapcar #'ebox-region-ids
(ebox-tree-node-children node)))))
((eq type 'concat)
(apply #'append
(mapcar #'ebox-region-ids
@ -4171,7 +4174,7 @@ REGION-ID and CHANGED-KEYS describe one update; CHANGES describes a batch."
:content)
(stringp (cadr expanded))
(numberp (ebox-get candidate-box :width))
(null (plist-get candidate-box :ebox-content-node))
(null (ebox-tree-node-children candidate-box))
(not (ebox-style-cascade-active-p))
(not (plist-get state :cascade-required-p))
(null (plist-get state :scroll-region-ids))))
@ -4587,7 +4590,9 @@ through dirty-set and patch-set execution before falling back to root rerender."
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

View File

@ -16,6 +16,14 @@
"Return rendered display pixel widths for NODE line by line."
(mapcar #'ebox--string-pixel-width (ebox-string-lines (ebox-render node))))
(defun ebox-dsl-test--tree-count (node predicate)
"Count nodes below NODE for which PREDICATE returns non-nil."
(if (or (stringp node) (not (listp node)))
0
(+ (if (funcall predicate node) 1 0)
(cl-loop for child in (ebox-tree-node-children node)
sum (ebox-dsl-test--tree-count child predicate)))))
(ert-deftest ebox-style-expands-ebox-aliases-to-canonical-longhands ()
"Existing Ebox aliases should normalize to CSS-like longhand properties."
(let ((style (ebox-style-compute
@ -172,6 +180,86 @@
(should (equal (ebox-box-node-children box) (list text)))
(should (string= (ebox-dsl-test--plain box) "A"))))
(ert-deftest ebox-canonical-row-box-retains-and-renders-typed-children ()
"A Row Box should retain one Box identity around all typed children."
(let* ((left (ebox-text-create :value "A" :source-handle 'left))
(right (ebox-text-create :value "B" :source-handle 'right))
(box (ebox-box-create :layout (ebox-row-layout-create)
:children (list left right)
:source-handle 'row)))
(should (ebox-box-node-p box))
(should (eq (ebox-node-source-handle box) 'row))
(should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'row))
(should (equal (ebox-box-node-children box) (list left right)))
(should (eq (ebox-tree-node-children box)
(ebox-box-node-children box)))
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 3))
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3))
(should-not (plist-member box :ebox-content-node))
(should (equal (ebox--computed-display box) '(block row)))
(should (string= (ebox-dsl-test--plain box) "AB"))))
(ert-deftest ebox-canonical-column-box-retains-and-renders-typed-children ()
"A Column Box should retain one Box identity around all typed children."
(let* ((top (ebox-text-create :value "A"))
(bottom (ebox-text-create :value "B"))
(box (ebox-box-create :layout (ebox-column-layout-create)
:children (list top bottom))))
(should (ebox-box-node-p box))
(should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'column))
(should (equal (ebox-box-node-children box) (list top bottom)))
(should (eq (ebox-tree-node-children box)
(ebox-box-node-children box)))
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 3))
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3))
(should-not (plist-member box :ebox-content-node))
(should (equal (ebox--computed-display box) '(block column)))
(should (string= (ebox-dsl-test--plain box) "A\nB"))))
(ert-deftest ebox-canonical-row-and-column-never-collapse-single-child-boxes ()
"A one-child Row or Column should preserve its canonical Box boundary."
(let* ((child (ebox-text-create :value "A"))
(row (ebox-box-create :layout (ebox-row-layout-create)
:children (list child)))
(column (ebox-box-create :layout (ebox-column-layout-create)
:children (list child))))
(should (ebox-box-node-p row))
(should (ebox-box-node-p column))
(should-not (eq row child))
(should-not (eq column child))
(should (equal (ebox-tree-node-children row) (list child)))
(should (equal (ebox-tree-node-children column) (list child)))
(should (= (ebox-dsl-test--tree-count row (lambda (_node) t)) 2))
(should (= (ebox-dsl-test--tree-count column (lambda (_node) t)) 2))
(should (string= (ebox-dsl-test--plain row) "A"))
(should (string= (ebox-dsl-test--plain column) "A"))))
(ert-deftest ebox-canonical-empty-row-and-column-have-no-runtime-child ()
"Empty Row and Column Boxes should not synthesize layout identities."
(dolist (layout (list (ebox-row-layout-create)
(ebox-column-layout-create)))
(let ((box (ebox-box-create :layout layout :children nil)))
(should (ebox-box-node-p box))
(should-not (ebox-tree-node-children box))
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 1))
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 1))
(should (string= (ebox-dsl-test--plain box) "")))))
(ert-deftest ebox-canonical-box-copy-keeps-one-authoritative-child-list ()
"A copied Box should expose exactly its copied runtime children."
(let* ((left (ebox-text-create :value "A"))
(right (ebox-text-create :value "B"))
(source (ebox-box-create :layout (ebox-row-layout-create)
:children (list left right)))
(copy (ebox-tree-copy-node-structure source))
(copied-children (ebox-tree-node-children copy)))
(should (eq copied-children (ebox-box-node-children copy)))
(should-not (eq copied-children (ebox-box-node-children source)))
(should-not (eq (car copied-children) left))
(should-not (eq (cadr copied-children) right))
(should (= (ebox-dsl-test--tree-count copy (lambda (_node) t)) 3))
(should (= (ebox-dsl-test--tree-count copy #'ebox-node-kind) 3))))
(ert-deftest ebox-canonical-box-rejects-paint-only-properties ()
"Canonical Box geometry should not duplicate paint-only state."
(should-error

View File

@ -170,6 +170,8 @@
("ebox-grid-item" . "defun")
("ebox-grid" . "defun")
("ebox-normal-layout-create" . "defun")
("ebox-row-layout-create" . "defun")
("ebox-column-layout-create" . "defun")
("ebox-text-create" . "defun")
("ebox-box-create" . "defun")
("ebox-build" . "defun")
@ -208,6 +210,8 @@
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