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 | | 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-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-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. | | `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-create`、`ebox-build` | 迁移期间创建旧节点或编译 `.ebox` list DSL。 |
| `ebox-concat`、`ebox-stack`、`ebox-row`、`ebox-column`、`ebox-spacer` | 组合简单横向/纵向布局与空白 box。 | | `ebox-concat`、`ebox-stack`、`ebox-row`、`ebox-column`、`ebox-spacer` | 组合简单横向/纵向布局与空白 box。 |
| `ebox-flex`、`ebox-flex-item` | 构建 Flex 容器与 item metadata。 | | `ebox-flex`、`ebox-flex-item` | 构建 Flex 容器与 item metadata。 |

View File

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

View File

@ -570,8 +570,8 @@ grapheme."
('box ('box
(when-let* ((region-id (ebox-get node :region-id))) (when-let* ((region-id (ebox-get node :region-id)))
(puthash region-id node ebox--region-box-table)) (puthash region-id node ebox--region-box-table))
(ebox--flex-recache-source-boxes (dolist (child (ebox-tree-node-children node))
(plist-get node :ebox-content-node))) (ebox--flex-recache-source-boxes child)))
('concat ('concat
(dolist (child (ebox--layout-children node)) (dolist (child (ebox--layout-children node))
(ebox--flex-recache-source-boxes child))) (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." "Return non-nil when path-copied NODE keeps the published line count."
(and (eq (plist-get node :ebox-type) 'box) (and (eq (plist-get node :ebox-type) 'box)
(numberp (ebox-get node :width)) (numberp (ebox-get node :width))
(null (plist-get node :ebox-content-node)) (null (ebox-tree-node-children node))
(= (length spans) (= (length spans)
(with-current-buffer buffer (with-current-buffer buffer
(length (ebox-string-lines (ebox--format-content node))))))) (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-grid "ebox-grid" (node))
(declare-function ebox--render-flex "ebox-flex" (node)) (declare-function ebox--render-flex "ebox-flex" (node))
(declare-function ebox-layout-config-kind "ebox-canonical" (config))
(declare-function ebox--render-with-cache (declare-function ebox--render-with-cache
"ebox-incremental" (node &optional force cache-probe)) "ebox-incremental" (node &optional force cache-probe))
(declare-function ebox-create "ebox" (&rest plist)) (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) (when (and (not ebox--flat-preformatted-box-fast-path-disabled)
(not (eq (ebox-get box :visibility) 'hidden)) (not (eq (ebox-get box :visibility) 'hidden))
(null (ebox--box-content-node box)) (null (ebox--box-content-node box))
(not (plist-member box :children))
(stringp (ebox-get box :content)) (stringp (ebox-get box :content))
(eq (ebox-get box :vertical-align) 'top) (eq (ebox-get box :vertical-align) 'top)
(equal (or (ebox-get box :min-height) 0) 0) (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." "Return BOX's lazy child layout node, if any."
(plist-get box :ebox-content-node)) (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 () (defun ebox--box-content-cache-context ()
"Return the current render context for lazy box content caching." "Return the current render context for lazy box content caching."
(list ebox-viewport-width (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--inline-auto-width-intrinsic-p
ebox--render-region-id)) ebox--render-region-id))
(defun ebox--render-box-content-node (box node) (defun ebox--render-box-content-cached (box renderer)
"Render NODE as BOX content, using the render-pass cache when available." "Return BOX content from RENDERER, reusing the render-pass cache."
(let* ((content-viewport (ebox--wrapper-content-viewport-pixel box))
(content
(if (null ebox--box-content-render-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)) (let* ((context (ebox--box-content-cache-context))
(entries (gethash box ebox--box-content-render-cache)) (entries (gethash box ebox--box-content-render-cache))
(cached (assoc context entries))) (cached (assoc context entries)))
(if cached (if cached
(cdr cached) (cdr cached)
(let ((rendered (let ((rendered (funcall renderer)))
(ebox--render-node-as-box-content node box)))
(puthash box (puthash box
(cons (cons context rendered) entries) (cons (cons context rendered) entries)
ebox--box-content-render-cache) 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 ;; A child layout has already resolved wrapping, alignment, and overflow
;; against CONTENT-VIEWPORT. Preserve that fact on the owning box so the ;; against CONTENT-VIEWPORT. Preserve that fact on the owning box so the
;; generic string formatter does not traverse the same propertized output ;; generic string formatter does not traverse the same propertized output
;; a second time. Intrinsic measurement is deliberately excluded: it ;; a second time. Intrinsic measurement is deliberately excluded: it
;; produces a natural-width probe, not final box content. ;; produces a natural-width probe, not final box content.
(when (and content-viewport (ebox--record-box-content-width-exact
(not (eq (and (listp node) (plist-get node :ebox-type)) box content
'box)) (not (eq (and (listp node) (plist-get node :ebox-type)) 'box)))
(not ebox--intrinsic-layout-measurement) content))
(equal (gethash content ebox--rendered-uniform-width-table)
content-viewport)) (defun ebox--render-box-layout-children (box)
(plist-put box :ebox-content-width-exact-p t)) "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)) content))
(defun ebox--box-content (box) (defun ebox--box-content (box)
"Return BOX content, rendering any lazy child layout content if present." "Return BOX content, rendering any lazy child layout content if present."
(ebox--propertize-typography (ebox--propertize-typography
(or (when-let* ((node (ebox--box-content-node box))) (cond
(ebox--render-box-content-node box node)) ((plist-member box :children)
(ebox-get box :content)) (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)) box))
(defun ebox--line-min-content-pixel (line) (defun ebox--line-min-content-pixel (line)
@ -1039,6 +1082,21 @@ property parsers."
(min (or max-width 999999999) (min (or max-width 999999999)
(max 0 preferred-width)))))) (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) (defun ebox--render-node-as-box-content (node props)
"Render NODE as preformatted content for a box with PROPS. "Render NODE as preformatted content for a box with PROPS.
When PROPS establish a content-box viewport, nested auto and `(viewport)' 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 viewport. A max-content wrapper instead measures the composite NODE without
an inherited viewport." an inherited viewport."
(let ((content-viewport (ebox--wrapper-content-viewport-pixel props))) (let ((content-viewport (ebox--wrapper-content-viewport-pixel props)))
(cond (ebox--call-with-box-content-context
((eq (ebox-get props :width) 'max-content) props
(let ((ebox-viewport-width nil) (lambda ()
(ebox--intrinsic-layout-measurement t) (if (and content-viewport
(ebox--inline-auto-width-intrinsic-p nil)) (hash-table-p ebox--render-cache-table)
(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)
(not ebox--intrinsic-layout-measurement) (not ebox--intrinsic-layout-measurement)
(null ebox--render-region-id) (null ebox--render-region-id)
(fboundp 'ebox--render-cache-probe) (fboundp 'ebox--render-cache-probe)
@ -1077,9 +1127,7 @@ an inherited viewport."
:scroll-lookahead :scroll-lookahead
ebox--scroll-window-initial-lookahead-lines-override ebox--scroll-window-initial-lookahead-lines-override
:scroll-disabled ebox--scroll-window-render-disabled))) :scroll-disabled ebox--scroll-window-render-disabled)))
(ebox-render node)))) (ebox-render node))))))
(t
(ebox-render node)))))
(defun ebox--resolve-size-content-pixel (box value fallback) (defun ebox--resolve-size-content-pixel (box value fallback)
"Resolve a horizontal size VALUE to BOX content pixels. "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) :display '(block row)
:children nodes)))) :children nodes))))
(defun ebox--render-concat (node) (defun ebox--render-row-children (children)
"Render a concat NODE to a string (internal, called by `ebox-render'). "Render flat row CHILDREN to one multi-line string.
Auto-width children are rendered at intrinsic inline size while explicit Auto-width children are rendered at intrinsic inline size while explicit
viewport/definite widths keep their containing-block semantics. Shorter viewport/definite widths keep their containing-block semantics. Shorter
children are padded with blank lines so all reach the same height before children are padded with blank lines so all reach the same height before
@ -1716,7 +1764,7 @@ horizontal concatenation."
;; the ambient containing block. ;; the ambient containing block.
(let ((ebox--inline-auto-width-intrinsic-p t)) (let ((ebox--inline-auto-width-intrinsic-p t))
(ebox--render-with-cache child))) (ebox--render-with-cache child)))
(ebox--layout-children node))) children))
(max-h (if rendered (max-h (if rendered
(apply #'max (mapcar #'ebox-string-height rendered)) (apply #'max (mapcar #'ebox-string-height rendered))
0)) 0))
@ -1729,6 +1777,10 @@ horizontal concatenation."
(ebox-lines-join (apply #'cl-mapcar #'concat line-lists)) (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 ;;;###autoload
(defun ebox-stack (node1 node2) (defun ebox-stack (node1 node2)
"Return a lazy stack node that places NODE1 above NODE2 vertically. "Return a lazy stack node that places NODE1 above NODE2 vertically.
@ -3074,8 +3126,8 @@ only used for incomplete lazy prefixes."
formatted-lines formatted-lines
(seq-take formatted-lines target-lines)))))))))) (seq-take formatted-lines target-lines))))))))))
(defun ebox--render-stack (node) (defun ebox--render-column-children (children)
"Render a stack NODE to a string (internal, called by `ebox-render'). "Render flat column CHILDREN to one multi-line string.
Stack leaves are rendered independently. When a containing-block viewport is Stack leaves are rendered independently. When a containing-block viewport is
bound, narrower rows are padded to that available width, while wider definite 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))) (let ((rendered (ebox--render-with-cache leaf)))
(cons rendered (cons rendered
(ebox--string-max-pixel-width rendered)))) (ebox--string-max-pixel-width rendered))))
(ebox--stack-leaves node))) children))
(max-w (if rendered-items (max-w (if rendered-items
(apply #'max (mapcar #'cdr rendered-items)) (apply #'max (mapcar #'cdr rendered-items))
0)) 0))
@ -3115,6 +3167,10 @@ ambient horizontal viewport."
(ebox--record-rendered-uniform-width rendered target-w) (ebox--record-rendered-uniform-width rendered target-w)
rendered)))) 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) (provide 'ebox-layout)
;;; ebox-layout.el ends here ;;; 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 (defun ebox-native-reflow--compile-box
(box &optional child-override child-override-p child-resolved-p) (box &optional child-override child-override-p child-resolved-p)
"Compile normalized Ebox BOX to strict layout IR." "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* ((source-child (plist-get box :ebox-content-node))
(child (if (or child-override-p child-resolved-p) (child (if (or child-override-p child-resolved-p)
child-override child-override
@ -1910,8 +1912,9 @@ backend has a matching two-dimensional layout contract."
((plist-get node :ebox-child-sequence) nil) ((plist-get node :ebox-child-sequence) nil)
((eq (plist-get node :ebox-type) 'grid) nil) ((eq (plist-get node :ebox-type) 'grid) nil)
((eq (plist-get node :ebox-type) 'box) ((eq (plist-get node :ebox-type) 'box)
(and (not (plist-member node :children))
(ebox-native-reflow--native-node-supported-p (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)) ((memq (plist-get node :ebox-type) '(concat stack))
(cl-every #'ebox-native-reflow--native-node-supported-p (cl-every #'ebox-native-reflow--native-node-supported-p
(ebox--layout-children node))) (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) (defun ebox-surface--style-consuming-node-p (node)
"Return non-nil when NODE owns a paintable style surface." "Return non-nil when NODE owns a paintable style surface."
(and (eq (plist-get node :ebox-type) 'box) (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))) (> (length (or (plist-get node :content) "")) 0)))
(defun ebox-surface--inline-inheritance-required-p (root) (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." "Return NODE's direct child nodes without assigning runtime ids."
(pcase (and (listp node) (plist-get node :ebox-type)) (pcase (and (listp node) (plist-get node :ebox-type))
('box ('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 ('concat
(ebox-tree-layout-children node)) (ebox-tree-layout-children node))
('stack ('stack
@ -246,7 +248,8 @@ still copied while every nested value remains shared."
(setq copy (setq copy
(ebox-tree--replace-direct-child-property (ebox-tree--replace-direct-child-property
copy property replacements))) 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)) (plist-member copy :children))
(let* ((children (plist-get copy :children)) (let* ((children (plist-get copy :children))
(replaced (replaced
@ -393,10 +396,17 @@ Record its identities in NODE-ID-SET and REGION-ID-SET."
node node-id-set region-id-set))) node node-id-set region-id-set)))
(pcase (plist-get node :ebox-type) (pcase (plist-get node :ebox-type)
('box ('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))) (when-let* ((child (plist-get node :ebox-content-node)))
(plist-put shell :ebox-content-node (plist-put shell :ebox-content-node
(ebox-tree--runtime-identity-skeleton (ebox-tree--runtime-identity-skeleton
child node-id-set region-id-set)))) child node-id-set region-id-set)))))
((or 'concat 'stack) ((or 'concat 'stack)
(plist-put shell :children (plist-put shell :children
(mapcar (mapcar
@ -809,8 +819,10 @@ candidate runtime index agree."
"Return NODE's logical children without internal layout adapters." "Return NODE's logical children without internal layout adapters."
(pcase (and (listp node) (plist-get node :ebox-type)) (pcase (and (listp node) (plist-get node :ebox-type))
('box ('box
(if (plist-member node :children)
(plist-get node :children)
(when-let* ((content-node (plist-get node :ebox-content-node))) (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) ((or 'concat 'stack)
(ebox-tree--semantic-layout-leaves node)) (ebox-tree--semantic-layout-leaves node))
('flex ('flex
@ -924,9 +936,11 @@ not only the public update ids."
(let ((region-ids (let ((region-ids
(pcase (plist-get node :ebox-type) (pcase (plist-get node :ebox-type)
('box ('box
(cons (ebox--ensure-region-id node) (cons
(ebox-tree-node-all-region-ids (ebox--ensure-region-id node)
(plist-get node :ebox-content-node)))) (apply #'append
(mapcar #'ebox-tree-node-all-region-ids
(ebox-tree-node-children node)))))
('concat ('concat
(apply #'append (apply #'append
(mapcar #'ebox-tree-node-all-region-ids (mapcar #'ebox-tree-node-all-region-ids
@ -969,8 +983,10 @@ cannot be inspected and reports no conflict."
(pcase (plist-get node :ebox-type) (pcase (plist-get node :ebox-type)
('box ('box
(or (and inherited (plist-get node paint-key) t) (or (and inherited (plist-get node paint-key) t)
(ebox-tree-node-paint-conflict-p (cl-some
(plist-get node :ebox-content-node) paint-key t))) (lambda (child)
(ebox-tree-node-paint-conflict-p child paint-key t))
(ebox-tree-node-children node))))
('concat ('concat
(cl-some (lambda (child) (cl-some (lambda (child)
(ebox-tree-node-paint-conflict-p child paint-key t)) (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 ('box
(or (when (equal (ebox-get node :region-id) region-id) (or (when (equal (ebox-get node :region-id) region-id)
(list node)) (list node))
(when-let* ((path (ebox-tree-node-path-to-region (catch 'found
(plist-get node :ebox-content-node) (dolist (child (ebox-tree-node-children node))
region-id))) (when-let* ((path
(append path (list node))))) (ebox-tree-node-path-to-region child region-id)))
(throw 'found (append path (list node)))))
nil)))
('concat ('concat
(catch 'found (catch 'found
(dolist (child (ebox-tree-layout-children node)) (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) (pcase (plist-get node :ebox-type)
('box ('box
(or (ebox--box-visible-overflow-p node) (or (ebox--box-visible-overflow-p node)
(ebox-tree-node-visible-overflow-p (cl-some #'ebox-tree-node-visible-overflow-p
(plist-get node :ebox-content-node)))) (ebox-tree-node-children node))))
('concat ('concat
(cl-some #'ebox-tree-node-visible-overflow-p (cl-some #'ebox-tree-node-visible-overflow-p
(ebox-tree-layout-children node))) (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))) (let ((type (plist-get node :ebox-type)))
(cond (cond
((eq type 'box) ((eq type 'box)
(cons (ebox--ensure-region-id node) (cons
(ebox-region-ids (plist-get node :ebox-content-node)))) (ebox--ensure-region-id node)
(apply #'append
(mapcar #'ebox-region-ids
(ebox-tree-node-children node)))))
((eq type 'concat) ((eq type 'concat)
(apply #'append (apply #'append
(mapcar #'ebox-region-ids (mapcar #'ebox-region-ids
@ -4171,7 +4174,7 @@ REGION-ID and CHANGED-KEYS describe one update; CHANGES describes a batch."
:content) :content)
(stringp (cadr expanded)) (stringp (cadr expanded))
(numberp (ebox-get candidate-box :width)) (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 (ebox-style-cascade-active-p))
(not (plist-get state :cascade-required-p)) (not (plist-get state :cascade-required-p))
(null (plist-get state :scroll-region-ids)))) (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-candidate-patch-host-paint
ebox-clear-cache ebox-clear-cache
ebox-column ebox-column
ebox-column-layout-create
ebox-normal-layout-create ebox-normal-layout-create
ebox-row-layout-create
ebox-text-create ebox-text-create
ebox-box-create ebox-box-create
ebox-commit ebox-commit

View File

@ -16,6 +16,14 @@
"Return rendered display pixel widths for NODE line by line." "Return rendered display pixel widths for NODE line by line."
(mapcar #'ebox--string-pixel-width (ebox-string-lines (ebox-render node)))) (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 () (ert-deftest ebox-style-expands-ebox-aliases-to-canonical-longhands ()
"Existing Ebox aliases should normalize to CSS-like longhand properties." "Existing Ebox aliases should normalize to CSS-like longhand properties."
(let ((style (ebox-style-compute (let ((style (ebox-style-compute
@ -172,6 +180,86 @@
(should (equal (ebox-box-node-children box) (list text))) (should (equal (ebox-box-node-children box) (list text)))
(should (string= (ebox-dsl-test--plain box) "A")))) (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 () (ert-deftest ebox-canonical-box-rejects-paint-only-properties ()
"Canonical Box geometry should not duplicate paint-only state." "Canonical Box geometry should not duplicate paint-only state."
(should-error (should-error

View File

@ -170,6 +170,8 @@
("ebox-grid-item" . "defun") ("ebox-grid-item" . "defun")
("ebox-grid" . "defun") ("ebox-grid" . "defun")
("ebox-normal-layout-create" . "defun") ("ebox-normal-layout-create" . "defun")
("ebox-row-layout-create" . "defun")
("ebox-column-layout-create" . "defun")
("ebox-text-create" . "defun") ("ebox-text-create" . "defun")
("ebox-box-create" . "defun") ("ebox-box-create" . "defun")
("ebox-build" . "defun") ("ebox-build" . "defun")
@ -208,6 +210,8 @@
ebox-clear-cache ebox-clear-cache
ebox-column ebox-column
ebox-normal-layout-create ebox-normal-layout-create
ebox-row-layout-create
ebox-column-layout-create
ebox-text-create ebox-text-create
ebox-box-create ebox-box-create
ebox-commit ebox-commit