From 31838e4e0233713069d5ac7980bee7b0cd283951 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Wed, 26 Aug 2026 19:32:46 +0800 Subject: [PATCH] feat: drive default GridConfig through canonical Box --- docs/user/ebox-api-reference.en.md | 2 +- docs/user/ebox-api-reference.zh.md | 2 +- ebox-canonical.el | 3 +- ebox-grid.el | 89 ++++++++++++++++++++++------ ebox-layout-config.el | 5 ++ ebox-layout.el | 10 +++- ebox.el | 1 + tests/ebox-dsl-tests.el | 94 +++++++++++++++++++++++++++++- tests/ebox-package-tests.el | 2 + 9 files changed, 184 insertions(+), 24 deletions(-) diff --git a/docs/user/ebox-api-reference.en.md b/docs/user/ebox-api-reference.en.md index 1965694..bda620e 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-row-layout-create`, `ebox-column-layout-create`, `ebox-flex-layout-create`, `ebox-box-create` | Framework-integration port for typed TextNode, Normal/Row/Column/Flex LayoutConfig, and BoxNode values. | +| `ebox-text-create`, `ebox-normal-layout-create`, `ebox-row-layout-create`, `ebox-column-layout-create`, `ebox-flex-layout-create`, `ebox-grid-layout-create`, `ebox-box-create` | Framework-integration port for typed TextNode, Normal/Row/Column/Flex/Grid 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 77276e3..0e9e866 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-row-layout-create`、`ebox-column-layout-create`、`ebox-flex-layout-create`、`ebox-box-create` | 框架集成使用的 typed TextNode、Normal/Row/Column/Flex LayoutConfig 与 BoxNode 端口。 | +| `ebox-text-create`、`ebox-normal-layout-create`、`ebox-row-layout-create`、`ebox-column-layout-create`、`ebox-flex-layout-create`、`ebox-grid-layout-create`、`ebox-box-create` | 框架集成使用的 typed TextNode、Normal/Row/Column/Flex/Grid 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 68eb3f4..413fd1f 100644 --- a/ebox-canonical.el +++ b/ebox-canonical.el @@ -178,7 +178,8 @@ child. Row and Column layouts accept any number of children." children) (error "Ebox Box children must be canonical Text/Box nodes: %S" children)) - (unless (memq (ebox-layout-config-kind layout) '(normal row column flex)) + (unless (memq (ebox-layout-config-kind layout) + '(normal row column flex grid)) (error "Ebox Box Layout is not implemented: %S" (ebox-layout-config-kind layout))) (when (and (eq (ebox-layout-config-kind layout) 'normal) diff --git a/ebox-grid.el b/ebox-grid.el index 86dbbec..77c904d 100644 --- a/ebox-grid.el +++ b/ebox-grid.el @@ -12,6 +12,7 @@ (require 'cl-lib) (require 'seq) (require 'subr-x) +(require 'ebox-layout-config) (require 'ebox-layout) (require 'ebox-flex) @@ -31,7 +32,7 @@ (defconst ebox--grid-layout-prop-keys '(:grid-template-columns :grid-template-rows :grid-auto-columns :grid-auto-rows :grid-auto-flow - :grid-column-gap :grid-row-gap :gap + :column-gap :row-gap :grid-column-gap :grid-row-gap :gap :justify-items :align-items :justify-content :align-content) "Properties owned by the grid formatting context.") @@ -123,15 +124,39 @@ (defun ebox-grid--gap-pair (props) "Return normalized (ROW-GAP . COLUMN-GAP) from grid PROPS." (let* ((pair (ebox--flex-gap-pair (plist-get props :gap))) - (row (if (plist-member props :grid-row-gap) - (plist-get props :grid-row-gap) - (car pair))) - (column (if (plist-member props :grid-column-gap) - (plist-get props :grid-column-gap) - (cdr pair)))) + (row (cond + ((plist-member props :row-gap) (plist-get props :row-gap)) + ((plist-member props :grid-row-gap) + (plist-get props :grid-row-gap)) + (t (car pair)))) + (column (cond + ((plist-member props :column-gap) + (plist-get props :column-gap)) + ((plist-member props :grid-column-gap) + (plist-get props :grid-column-gap)) + (t (cdr pair))))) (cons (or (ebox--flex-line-value row 0) 0) (or (ebox--nonnegative-horizontal-size-pixels column 0) 0)))) +(defconst ebox--grid-default-config-props + '(:grid-template-columns nil :grid-template-rows nil + :grid-auto-columns nil :grid-auto-rows nil :grid-auto-flow row + :row-gap 0 :column-gap 0 + :justify-items stretch :align-items stretch + :justify-content start :align-content start) + "Validated property set for the first default GridConfig slice.") + +(defun ebox-grid-layout-config-props-p (props) + "Return non-nil when PROPS are the validated default GridConfig." + (equal props ebox--grid-default-config-props)) + +;;;###autoload +(defun ebox-grid-layout-create () + "Return the canonical Grid layout config with computed defaults." + (ebox-layout-config--create + :kind 'grid + :props (copy-sequence ebox--grid-default-config-props))) + (defun ebox-grid--visual-props-p (props) "Return non-nil when PROPS contains visible box behavior." (cl-loop for (key _value) on props by #'cddr @@ -618,10 +643,25 @@ size for start/center/end alignment unless it would overflow its track." (puthash region-id box ebox--region-box-table))) rendered)) -(defun ebox--render-grid (node) - "Render GRID NODE to a propertized string." - (let* ((props (plist-get node :raw-props)) - (children (ebox-tree-layout-children node)) +(defun ebox-grid--legacy-constraints (props wrapper) + "Return private GridConstraints from legacy PROPS and WRAPPER." + (let ((width (ebox--nonnegative-horizontal-size-pixels + (plist-get props :width) + (ebox--viewport-pixel-width nil))) + (height (ebox--flex-line-value (plist-get props :height) nil))) + (when wrapper + (setq width (ebox--box-sizing-content-pixel wrapper width))) + (list :width width :height height))) + +(defun ebox-grid--box-constraints (box) + "Project BOX content dimensions to private GridConstraints." + (list :width (ebox--wrapper-content-viewport-pixel box) + :height (ebox--resolve-size-content-height + box (ebox-get box :height) nil))) + +(defun ebox--render-grid-children (config constraints children) + "Render Grid CONFIG under frame CONSTRAINTS over flat CHILDREN." + (let* ((props config) (columns (ebox-grid--normalize-tracks (plist-get props :grid-template-columns) 'columns)) (rows (ebox-grid--normalize-tracks @@ -639,14 +679,9 @@ size for start/center/end alignment unless it would overflow its track." (matrix (nth 1 placed)) (row-count (nth 2 placed)) (column-count (nth 3 placed)) - (available (ebox--nonnegative-horizontal-size-pixels - (plist-get props :width) - (ebox--viewport-pixel-width nil))) - (wrapper (plist-get node :box)) - (height (ebox--flex-line-value (plist-get props :height) nil)) + (available (plist-get constraints :width)) + (height (plist-get constraints :height)) (rendered (make-hash-table :test 'eq))) - (when wrapper - (setq available (ebox--box-sizing-content-pixel wrapper available))) (unless (memq flow '(row column)) (error "ebox-grid: :grid-auto-flow must be `row' or `column'")) (dolist (entry entries) @@ -674,7 +709,23 @@ size for start/center/end alignment unless it would overflow its track." (or (plist-get props :align-content) 'start))) (body (ebox-grid--render entries matrix column-layout row-layout props sized-rendered))) - (ebox--render-grid-box node body)))) + body))) + +(defun ebox--render-grid-box-children (box config children) + "Render canonical BOX Grid CONFIG over flat CHILDREN." + (ebox--render-grid-children + config (ebox-grid--box-constraints box) children)) + +(defun ebox--render-grid (node) + "Render legacy GRID NODE to a propertized string." + (let* ((props (plist-get node :raw-props)) + (wrapper (plist-get node :box))) + (ebox--render-grid-box + node + (ebox--render-grid-children + (ebox--plist-keep-keys props ebox--grid-layout-prop-keys) + (ebox-grid--legacy-constraints props wrapper) + (ebox-tree-layout-children node))))) ;;;###autoload (defun ebox-grid (&rest items) diff --git a/ebox-layout-config.el b/ebox-layout-config.el index 2099adb..cee5c0b 100644 --- a/ebox-layout-config.el +++ b/ebox-layout-config.el @@ -11,6 +11,8 @@ (declare-function ebox-flex-layout-config-props-p "ebox-flex" (props)) +(declare-function ebox-grid-layout-config-props-p + "ebox-grid" (props)) (cl-defstruct (ebox-layout-config @@ -31,6 +33,9 @@ ('flex (and (fboundp 'ebox-flex-layout-config-props-p) (ebox-flex-layout-config-props-p props))) + ('grid + (and (fboundp 'ebox-grid-layout-config-props-p) + (ebox-grid-layout-config-props-p props))) (_ nil)))) (unless valid-p (error "Invalid %S LayoutConfig properties: %S" diff --git a/ebox-layout.el b/ebox-layout.el index 1790c01..920995b 100644 --- a/ebox-layout.el +++ b/ebox-layout.el @@ -17,6 +17,8 @@ (require 'ebox-buffer-backend) (declare-function ebox--render-grid "ebox-grid" (node)) +(declare-function ebox--render-grid-box-children + "ebox-grid" (box props children)) (declare-function ebox--render-flex "ebox-flex" (node)) (declare-function ebox--render-flex-box-children "ebox-flex" (box props children)) @@ -965,9 +967,15 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values." (ebox-layout-config-props (plist-get box :ebox-layout-config)) children)) + ('grid + (ebox--render-grid-box-children + box + (ebox-layout-config-props + (plist-get box :ebox-layout-config)) + children)) (_ (error "Ebox Box has unsupported LayoutConfig: %S" kind))))))) (ebox--record-box-content-width-exact - box content (memq kind '(row column flex))) + box content (memq kind '(row column flex grid))) content)) (defun ebox--box-content (box) diff --git a/ebox.el b/ebox.el index e30096b..935ab5e 100644 --- a/ebox.el +++ b/ebox.el @@ -4607,6 +4607,7 @@ through dirty-set and patch-set execution before falling back to root rerender." ebox-flex-item ebox-grid ebox-grid-fr + ebox-grid-layout-create ebox-grid-item ebox-host-ref-bounds ebox-host-ref-position diff --git a/tests/ebox-dsl-tests.el b/tests/ebox-dsl-tests.el index 6a5be41..9bca977 100644 --- a/tests/ebox-dsl-tests.el +++ b/tests/ebox-dsl-tests.el @@ -312,6 +312,97 @@ (should (= (ebox--string-pixel-width typed-rendered) (ebox--string-pixel-width legacy-rendered)))))) +(ert-deftest ebox-canonical-grid-box-uses-one-box-identity-and-child-list () + "A GridConfig should select the Box algorithm without a Grid runtime node." + (let* ((layout (ebox-grid-layout-create)) + (box (ebox-box-create + :layout layout + :children (list (ebox-text-create :value "A") + (ebox-text-create :value "B")) + :width '(120))) + (rendered (ebox-render box))) + (should (equal (ebox-layout-config-props layout) + ebox--grid-default-config-props)) + (should-not (plist-member (ebox-layout-config-props layout) :width)) + (should-not (plist-member (ebox-layout-config-props layout) :height)) + (should (ebox-box-node-p box)) + (should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'grid)) + (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 + (= (ebox-dsl-test--tree-count + box (lambda (node) (eq (plist-get node :ebox-type) 'grid))) + 0)) + (should-not (plist-member box :ebox-content-node)) + (should (equal (ebox--computed-display box) '(block grid))) + (should (string= + (replace-regexp-in-string + "[[:space:]]" "" (substring-no-properties rendered)) + "AB")) + (should (equal (mapcar #'ebox--string-pixel-width + (ebox-string-lines rendered)) + '(120 120 120))))) + +(ert-deftest ebox-canonical-grid-consumes-direct-child-placement () + "Grid placement should stay on child Boxes without grid-item wrappers." + (let* ((later + (ebox-box-create :layout (ebox-normal-layout-create) + :children (list (ebox-text-create :value "A")) + :grid-row 2)) + (earlier + (ebox-box-create :layout (ebox-normal-layout-create) + :children (list (ebox-text-create :value "B")) + :grid-row 1)) + (box (ebox-box-create :layout (ebox-grid-layout-create) + :children (list later earlier))) + (rendered (substring-no-properties (ebox-render box)))) + (should (string= + (replace-regexp-in-string "[[:space:]]" "" rendered) + "BA")) + (should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 5)) + (should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 5)))) + +(ert-deftest ebox-canonical-grid-revalidates-the-typed-config-boundary () + "GridConfig must reject frame properties and cross-kind property sets." + (let ((extra-frame-prop (ebox-grid-layout-create)) + (cross-kind-props (ebox-grid-layout-create))) + (setf (ebox-layout-config-props extra-frame-prop) + (append (ebox-layout-config-props extra-frame-prop) + '(:width 999))) + (setf (ebox-layout-config-kind cross-kind-props) 'column) + (should-error + (ebox-box-create :layout extra-frame-prop :children nil) + :type 'error) + (should-error + (ebox-box-create :layout cross-kind-props :children nil) + :type 'error))) + +(ert-deftest ebox-canonical-grid-projects-one-box-frame-constraint () + "Typed and legacy Grid should derive the same content size from BoxFrame." + (dolist (sizing '(border-box content-box)) + (let* ((typed + (ebox-box-create + :layout (ebox-grid-layout-create) + :children (list (ebox-text-create :value "A") + (ebox-text-create :value "B")) + :width '(140) :padding '(0 (10)) :box-sizing sizing)) + (legacy + (ebox-grid + :width '(140) :padding '(0 (10)) :box-sizing sizing + (ebox-text-create :value "A") + (ebox-text-create :value "B"))) + (typed-rendered (ebox-render typed)) + (legacy-rendered (ebox-render legacy))) + (should (string= (substring-no-properties typed-rendered) + (substring-no-properties legacy-rendered))) + (should + (equal (mapcar #'ebox--string-pixel-width + (ebox-string-lines typed-rendered)) + (mapcar #'ebox--string-pixel-width + (ebox-string-lines legacy-rendered))))))) + (ert-deftest ebox-canonical-flex-consumes-direct-child-participation () "Flex participation should stay on child Boxes without item wrappers." (let* ((left-text (ebox-text-create :value "A")) @@ -355,7 +446,8 @@ "Empty layout Boxes should not synthesize layout identities." (dolist (layout (list (ebox-row-layout-create) (ebox-column-layout-create) - (ebox-flex-layout-create))) + (ebox-flex-layout-create) + (ebox-grid-layout-create))) (let ((box (ebox-box-create :layout layout :children nil))) (should (ebox-box-node-p box)) (should-not (ebox-tree-node-children box)) diff --git a/tests/ebox-package-tests.el b/tests/ebox-package-tests.el index 39a71a3..3e1ef6d 100644 --- a/tests/ebox-package-tests.el +++ b/tests/ebox-package-tests.el @@ -172,6 +172,7 @@ ("ebox-grid-fr" . "defun") ("ebox-grid-item" . "defun") ("ebox-grid" . "defun") + ("ebox-grid-layout-create" . "defun") ("ebox-normal-layout-create" . "defun") ("ebox-row-layout-create" . "defun") ("ebox-column-layout-create" . "defun") @@ -228,6 +229,7 @@ ebox-flex-item ebox-grid ebox-grid-fr + ebox-grid-layout-create ebox-grid-item ebox-host-ref-bounds ebox-host-ref-position