From 53001719c4a2a8d1e9dadd1504198d5a55bb4969 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Wed, 26 Aug 2026 19:03:35 +0800 Subject: [PATCH] feat: retain Row and Column within canonical Box identity --- docs/user/ebox-api-reference.en.md | 2 +- docs/user/ebox-api-reference.zh.md | 2 +- ebox-canonical.el | 50 +++++--- ebox-flex.el | 4 +- ebox-incremental.el | 2 +- ebox-layout.el | 184 +++++++++++++++++++---------- ebox-native-reflow.el | 7 +- ebox-surface.el | 2 +- ebox-tree.el | 56 ++++++--- ebox.el | 11 +- tests/ebox-dsl-tests.el | 88 ++++++++++++++ tests/ebox-package-tests.el | 4 + 12 files changed, 301 insertions(+), 111 deletions(-) diff --git a/docs/user/ebox-api-reference.en.md b/docs/user/ebox-api-reference.en.md index 881ce30..6ee33b3 100644 --- a/docs/user/ebox-api-reference.en.md +++ b/docs/user/ebox-api-reference.en.md @@ -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. | diff --git a/docs/user/ebox-api-reference.zh.md b/docs/user/ebox-api-reference.zh.md index 1bd119e..8ce6982 100644 --- a/docs/user/ebox-api-reference.zh.md +++ b/docs/user/ebox-api-reference.zh.md @@ -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。 | diff --git a/ebox-canonical.el b/ebox-canonical.el index 98b6f46..634591a 100644 --- a/ebox-canonical.el +++ b/ebox-canonical.el @@ -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) diff --git a/ebox-flex.el b/ebox-flex.el index 43b2df6..bc05c5e 100644 --- a/ebox-flex.el +++ b/ebox-flex.el @@ -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))) diff --git a/ebox-incremental.el b/ebox-incremental.el index 0a2e05a..ec8b1f2 100644 --- a/ebox-incremental.el +++ b/ebox-incremental.el @@ -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))))))) diff --git a/ebox-layout.el b/ebox-layout.el index e2f32d4..977e90c 100644 --- a/ebox-layout.el +++ b/ebox-layout.el @@ -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-cached (box renderer) + "Return BOX content from RENDERER, reusing the render-pass cache." + (if (null ebox--box-content-render-cache) + (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 (funcall renderer))) + (puthash box + (cons (cons context rendered) entries) + ebox--box-content-render-cache) + 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-viewport (ebox--wrapper-content-viewport-pixel box)) - (content - (if (null ebox--box-content-render-cache) - (ebox--render-node-as-box-content node box) - (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))) - (puthash box - (cons (cons context rendered) entries) - ebox--box-content-render-cache) - rendered)))))) + (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,40 +1104,30 @@ 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) - (not ebox--intrinsic-layout-measurement) - (null ebox--render-region-id) - (fboundp 'ebox--render-cache-probe) - (fboundp 'ebox--render-with-cache)) - (ebox--render-with-cache + (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) + (fboundp 'ebox--render-with-cache)) + (ebox--render-with-cache + node nil + (ebox--render-cache-probe node nil - (ebox--render-cache-probe - node nil - (list :box-content t - :content-viewport content-viewport - :viewport-height ebox-viewport-height - :intrinsic ebox--intrinsic-layout-measurement - :auto-width-intrinsic - ebox--inline-auto-width-intrinsic-p - :render-region-id ebox--render-region-id - :scroll-lookahead - ebox--scroll-window-initial-lookahead-lines-override - :scroll-disabled ebox--scroll-window-render-disabled))) - (ebox-render node)))) - (t - (ebox-render node))))) + (list :box-content t + :content-viewport content-viewport + :viewport-height ebox-viewport-height + :intrinsic ebox--intrinsic-layout-measurement + :auto-width-intrinsic + ebox--inline-auto-width-intrinsic-p + :render-region-id ebox--render-region-id + :scroll-lookahead + ebox--scroll-window-initial-lookahead-lines-override + :scroll-disabled ebox--scroll-window-render-disabled))) + (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 diff --git a/ebox-native-reflow.el b/ebox-native-reflow.el index b9c2730..2eaf81e 100644 --- a/ebox-native-reflow.el +++ b/ebox-native-reflow.el @@ -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) - (ebox-native-reflow--native-node-supported-p - (plist-get node :ebox-content-node))) + (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))) diff --git a/ebox-surface.el b/ebox-surface.el index 93639ae..646daa3 100644 --- a/ebox-surface.el +++ b/ebox-surface.el @@ -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) diff --git a/ebox-tree.el b/ebox-tree.el index 45a3e6e..0307cf0 100644 --- a/ebox-tree.el +++ b/ebox-tree.el @@ -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 - (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)))) + (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))))) ((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 - (when-let* ((content-node (plist-get node :ebox-content-node))) - (ebox-tree--semantic-layout-leaves content-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)))) ((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))) diff --git a/ebox.el b/ebox.el index c3c0eb2..43e8c50 100644 --- a/ebox.el +++ b/ebox.el @@ -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 diff --git a/tests/ebox-dsl-tests.el b/tests/ebox-dsl-tests.el index 64eb2e3..9f0ceac 100644 --- a/tests/ebox-dsl-tests.el +++ b/tests/ebox-dsl-tests.el @@ -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 diff --git a/tests/ebox-package-tests.el b/tests/ebox-package-tests.el index 8d48c46..df337b4 100644 --- a/tests/ebox-package-tests.el +++ b/tests/ebox-package-tests.el @@ -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