feat: drive default GridConfig through canonical Box

This commit is contained in:
Kinneyzhang 2026-08-26 19:32:46 +08:00
parent e7dcb55d61
commit 31838e4e02
9 changed files with 184 additions and 24 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-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-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-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-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

@ -178,7 +178,8 @@ child. Row and Column layouts accept any number of children."
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 (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" (error "Ebox Box Layout is not implemented: %S"
(ebox-layout-config-kind layout))) (ebox-layout-config-kind layout)))
(when (and (eq (ebox-layout-config-kind layout) 'normal) (when (and (eq (ebox-layout-config-kind layout) 'normal)

View File

@ -12,6 +12,7 @@
(require 'cl-lib) (require 'cl-lib)
(require 'seq) (require 'seq)
(require 'subr-x) (require 'subr-x)
(require 'ebox-layout-config)
(require 'ebox-layout) (require 'ebox-layout)
(require 'ebox-flex) (require 'ebox-flex)
@ -31,7 +32,7 @@
(defconst ebox--grid-layout-prop-keys (defconst ebox--grid-layout-prop-keys
'(:grid-template-columns :grid-template-rows '(:grid-template-columns :grid-template-rows
:grid-auto-columns :grid-auto-rows :grid-auto-flow :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) :justify-items :align-items :justify-content :align-content)
"Properties owned by the grid formatting context.") "Properties owned by the grid formatting context.")
@ -123,15 +124,39 @@
(defun ebox-grid--gap-pair (props) (defun ebox-grid--gap-pair (props)
"Return normalized (ROW-GAP . COLUMN-GAP) from grid PROPS." "Return normalized (ROW-GAP . COLUMN-GAP) from grid PROPS."
(let* ((pair (ebox--flex-gap-pair (plist-get props :gap))) (let* ((pair (ebox--flex-gap-pair (plist-get props :gap)))
(row (if (plist-member props :grid-row-gap) (row (cond
(plist-get props :grid-row-gap) ((plist-member props :row-gap) (plist-get props :row-gap))
(car pair))) ((plist-member props :grid-row-gap)
(column (if (plist-member props :grid-column-gap) (plist-get props :grid-row-gap))
(plist-get props :grid-column-gap) (t (car pair))))
(cdr 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) (cons (or (ebox--flex-line-value row 0) 0)
(or (ebox--nonnegative-horizontal-size-pixels column 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) (defun ebox-grid--visual-props-p (props)
"Return non-nil when PROPS contains visible box behavior." "Return non-nil when PROPS contains visible box behavior."
(cl-loop for (key _value) on props by #'cddr (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))) (puthash region-id box ebox--region-box-table)))
rendered)) rendered))
(defun ebox--render-grid (node) (defun ebox-grid--legacy-constraints (props wrapper)
"Render GRID NODE to a propertized string." "Return private GridConstraints from legacy PROPS and WRAPPER."
(let* ((props (plist-get node :raw-props)) (let ((width (ebox--nonnegative-horizontal-size-pixels
(children (ebox-tree-layout-children node)) (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 (columns (ebox-grid--normalize-tracks
(plist-get props :grid-template-columns) 'columns)) (plist-get props :grid-template-columns) 'columns))
(rows (ebox-grid--normalize-tracks (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)) (matrix (nth 1 placed))
(row-count (nth 2 placed)) (row-count (nth 2 placed))
(column-count (nth 3 placed)) (column-count (nth 3 placed))
(available (ebox--nonnegative-horizontal-size-pixels (available (plist-get constraints :width))
(plist-get props :width) (height (plist-get constraints :height))
(ebox--viewport-pixel-width nil)))
(wrapper (plist-get node :box))
(height (ebox--flex-line-value (plist-get props :height) nil))
(rendered (make-hash-table :test 'eq))) (rendered (make-hash-table :test 'eq)))
(when wrapper
(setq available (ebox--box-sizing-content-pixel wrapper available)))
(unless (memq flow '(row column)) (unless (memq flow '(row column))
(error "ebox-grid: :grid-auto-flow must be `row' or `column'")) (error "ebox-grid: :grid-auto-flow must be `row' or `column'"))
(dolist (entry entries) (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))) (or (plist-get props :align-content) 'start)))
(body (ebox-grid--render entries matrix column-layout row-layout (body (ebox-grid--render entries matrix column-layout row-layout
props sized-rendered))) 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 ;;;###autoload
(defun ebox-grid (&rest items) (defun ebox-grid (&rest items)

View File

@ -11,6 +11,8 @@
(declare-function ebox-flex-layout-config-props-p (declare-function ebox-flex-layout-config-props-p
"ebox-flex" (props)) "ebox-flex" (props))
(declare-function ebox-grid-layout-config-props-p
"ebox-grid" (props))
(cl-defstruct (cl-defstruct
(ebox-layout-config (ebox-layout-config
@ -31,6 +33,9 @@
('flex ('flex
(and (fboundp 'ebox-flex-layout-config-props-p) (and (fboundp 'ebox-flex-layout-config-props-p)
(ebox-flex-layout-config-props-p props))) (ebox-flex-layout-config-props-p props)))
('grid
(and (fboundp 'ebox-grid-layout-config-props-p)
(ebox-grid-layout-config-props-p props)))
(_ nil)))) (_ nil))))
(unless valid-p (unless valid-p
(error "Invalid %S LayoutConfig properties: %S" (error "Invalid %S LayoutConfig properties: %S"

View File

@ -17,6 +17,8 @@
(require 'ebox-buffer-backend) (require 'ebox-buffer-backend)
(declare-function ebox--render-grid "ebox-grid" (node)) (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 "ebox-flex" (node))
(declare-function ebox--render-flex-box-children (declare-function ebox--render-flex-box-children
"ebox-flex" (box props 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 (ebox-layout-config-props
(plist-get box :ebox-layout-config)) (plist-get box :ebox-layout-config))
children)) 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))))))) (_ (error "Ebox Box has unsupported LayoutConfig: %S" kind)))))))
(ebox--record-box-content-width-exact (ebox--record-box-content-width-exact
box content (memq kind '(row column flex))) box content (memq kind '(row column flex grid)))
content)) content))
(defun ebox--box-content (box) (defun ebox--box-content (box)

View File

@ -4607,6 +4607,7 @@ through dirty-set and patch-set execution before falling back to root rerender."
ebox-flex-item ebox-flex-item
ebox-grid ebox-grid
ebox-grid-fr ebox-grid-fr
ebox-grid-layout-create
ebox-grid-item ebox-grid-item
ebox-host-ref-bounds ebox-host-ref-bounds
ebox-host-ref-position ebox-host-ref-position

View File

@ -312,6 +312,97 @@
(should (= (ebox--string-pixel-width typed-rendered) (should (= (ebox--string-pixel-width typed-rendered)
(ebox--string-pixel-width legacy-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 () (ert-deftest ebox-canonical-flex-consumes-direct-child-participation ()
"Flex participation should stay on child Boxes without item wrappers." "Flex participation should stay on child Boxes without item wrappers."
(let* ((left-text (ebox-text-create :value "A")) (let* ((left-text (ebox-text-create :value "A"))
@ -355,7 +446,8 @@
"Empty layout Boxes should not synthesize layout identities." "Empty layout Boxes should not synthesize layout identities."
(dolist (layout (list (ebox-row-layout-create) (dolist (layout (list (ebox-row-layout-create)
(ebox-column-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))) (let ((box (ebox-box-create :layout layout :children nil)))
(should (ebox-box-node-p box)) (should (ebox-box-node-p box))
(should-not (ebox-tree-node-children box)) (should-not (ebox-tree-node-children box))

View File

@ -172,6 +172,7 @@
("ebox-grid-fr" . "defun") ("ebox-grid-fr" . "defun")
("ebox-grid-item" . "defun") ("ebox-grid-item" . "defun")
("ebox-grid" . "defun") ("ebox-grid" . "defun")
("ebox-grid-layout-create" . "defun")
("ebox-normal-layout-create" . "defun") ("ebox-normal-layout-create" . "defun")
("ebox-row-layout-create" . "defun") ("ebox-row-layout-create" . "defun")
("ebox-column-layout-create" . "defun") ("ebox-column-layout-create" . "defun")
@ -228,6 +229,7 @@
ebox-flex-item ebox-flex-item
ebox-grid ebox-grid
ebox-grid-fr ebox-grid-fr
ebox-grid-layout-create
ebox-grid-item ebox-grid-item
ebox-host-ref-bounds ebox-host-ref-bounds
ebox-host-ref-position ebox-host-ref-position