refactor: make Grid placement a direct Box role

This commit is contained in:
Kinneyzhang 2026-08-26 20:49:07 +08:00
parent 4b409f16f4
commit 6d9f2a4d82
8 changed files with 36 additions and 74 deletions

View File

@ -136,9 +136,7 @@ of full browser CSS compatibility.
:grid-template-columns '((200) 1fr 1fr)
:grid-template-rows '(1 1)
:gap '(1 (12))
(ebox-grid-item
(ebox-create :content "Header")
:grid-column '(1 :span 3))
(ebox-create :content "Header" :grid-column '(1 :span 3))
(ebox-create :content "Main" :grid-column 1 :grid-row 2)
(ebox-create :content "Aside" :grid-column 2 :grid-row 2))
```
@ -152,8 +150,8 @@ positions use implicit tracks. `:grid-auto-columns`, `:grid-auto-rows`, and
Grid item placement is one-based. `:grid-column` and `:grid-row` accept a
positive start integer, `(start :span positive-integer)`, or `(start end)` with
`end` greater than `start`. `:grid-column-span` and `:grid-row-span` accept
positive integers. `ebox-grid-item` is a convenience wrapper; placement
properties may also be placed directly on a child node.
positive integers. Placement properties belong directly to the child Box;
Grid item is a parent-child role, not a wrapper or constructor.
## 4. Units, box properties, and text properties
@ -534,15 +532,14 @@ boolean to nil when a host application must avoid background preparation.
## 10. Standalone `.ebox` DSL
`ebox-build` compiles a data-oriented list form. Supported tags are `box` and
`ebox`, `row`, `column`, `flex`, `item`, `grid`, `grid-item`, and `spacer`:
`ebox`, `row`, `column`, `flex`, `item`, `grid`, and `spacer`:
```elisp
(ebox-build
'(grid :width (640)
:grid-template-columns ((200) 1fr 1fr)
:gap (1 (12))
(grid-item :grid-column (1 :span 3)
(box :content "Header"))
(box :grid-column (1 :span 3) :content "Header")
(box :content "Main")))
```
@ -550,7 +547,7 @@ Strings directly under `box` become newline-joined content. Child forms become
a default vertical child layout. `row` and `column` can receive box
properties around their child layout. `flex` accepts container properties;
`item` accepts flex item properties and exactly one or more child forms;
`grid-item` requires exactly one child; `spacer` accepts no children.
`spacer` accepts no children.
Inside a `.ebox` file, keep structural forms unquoted. Quote list and symbol
constants in property positions when they are data, such as
@ -591,7 +588,7 @@ style-rule functions immediately below are public module-level style APIs.
| `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. |
| `ebox-grid`, `ebox-grid-fr`, `ebox-grid-item` | Build Grid layouts, fractional tracks, and placement metadata. |
| `ebox-grid`, `ebox-grid-fr` | Build Grid layouts and fractional tracks; placement metadata belongs to direct child Boxes. |
| `ebox-render`, `ebox-render-to-buffer`, `ebox-display-buffer` | Pure materialization, retained mount, and display wrapper. |
| `ebox-commit`, `ebox-buffer-update-report`, `ebox-rerender-buffer-with-context`, `ebox-viewport-window-width` | Atomic root commit, report lookup, viewport-context rerender, and the shared display-safe width sampler. |
| `ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update` | Region mapping, logical handle lookup, and direct update. |

View File

@ -124,9 +124,7 @@ Flex preferred/min/max size 支持 Ebox 子集:`auto`、`min-content`、
:grid-template-columns '((200) 1fr 1fr)
:grid-template-rows '(1 1)
:gap '(1 (12))
(ebox-grid-item
(ebox-create :content "Header")
:grid-column '(1 :span 3))
(ebox-create :content "Header" :grid-column '(1 :span 3))
(ebox-create :content "Main" :grid-column 1 :grid-row 2)
(ebox-create :content "Aside" :grid-column 2 :grid-row 2))
```
@ -138,8 +136,8 @@ Flex preferred/min/max size 支持 Ebox 子集:`auto`、`min-content`、
Grid placement 从 1 开始。`:grid-column` 和 `:grid-row` 接受正整数起点、
`(start :span 正整数)`,或 `start` 小于 `end``(start end)`
`:grid-column-span``:grid-row-span` 接受正整数。`ebox-grid-item` 是便捷
包装器placement property 也可以直接放在 child node 上
`:grid-column-span``:grid-row-span` 接受正整数。placement property 直接属于
child BoxGrid item 是父子参与角色,不是 wrapper 或 constructor
## 4. 单位、box property 与文本属性
@ -484,22 +482,20 @@ Idle prewarm 不会发布 buffer text 或 runtime state。宿主应用不希望
## 10. 独立 `.ebox` DSL
`ebox-build` 编译数据型 list form。支持的 tag 是 `box`/`ebox`、`row`、
`column`、`flex`、`item`、`grid`、`grid-item``spacer`
`column`、`flex`、`item`、`grid` 和 `spacer`
```elisp
(ebox-build
'(grid :width (640)
:grid-template-columns ((200) 1fr 1fr)
:gap (1 (12))
(grid-item :grid-column (1 :span 3)
(box :content "Header"))
(box :grid-column (1 :span 3) :content "Header")
(box :content "Main")))
```
`box` 下的直接字符串会合并为换行内容child form 会变成默认纵向 child
layout。`row` 与 `column` 可以接收 box property 并包住 child layout`flex`
接收容器 property`item` 接收 flex item property`grid-item` 必须有一个
child`spacer` 不能有 child。
接收容器 property`item` 接收 flex item property`spacer` 不能有 child。
`.ebox` 文件中,结构 form 保持不加 quoteproperty 位置的 list 和 symbol
常量要作为 data quote例如 `:gap '(1 (12))``:justify-content 'center`
@ -537,7 +533,7 @@ style API。
| `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。 |
| `ebox-grid`、`ebox-grid-fr`、`ebox-grid-item` | 构建 Grid、分数轨道和 placement metadata。 |
| `ebox-grid`、`ebox-grid-fr` | 构建 Grid 与分数轨道placement metadata 直接属于 child Box。 |
| `ebox-render`、`ebox-render-to-buffer`、`ebox-display-buffer` | 纯 materialization、retained mount 与展示包装。 |
| `ebox-commit`、`ebox-buffer-update-report`、`ebox-rerender-buffer-with-context`、`ebox-viewport-window-width` | 原子根提交、报告查询、viewport-context rerender 与统一的显示安全宽度采样。 |
| `ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update` | Region 映射、logical handle 查询与直接更新。 |

View File

@ -160,17 +160,6 @@ The first value is string content. The second value is a lazy child node."
(plist-put child key (plist-get item-props key))))
(apply #'ebox-flex-item child item-props))))
(defun ebox--build-grid-item (items)
"Build a grid-item node from DSL ITEMS."
(let* ((split (ebox-grid--split-attrs items))
(props (car split))
(children (delq nil (cdr split))))
(unless (= (length children) 1)
(error "ebox-build: grid-item requires exactly one child node"))
(apply #'ebox-grid-item
(ebox-dsl-build (car children))
props)))
(defun ebox--build-grid (items)
"Build a grid node from DSL ITEMS."
(let* ((split (ebox-grid--split-attrs items))
@ -226,7 +215,7 @@ Supported forms:
(row (box :content \"Left\") (spacer :width (16)) (box :content \"Right\"))
(column (box :content \"Top\") (box :content \"Bottom\"))
(grid :grid-template-columns ((80) (80))
(grid-item :grid-column (1 :span 2) (box \"Header\")))
(box :grid-column (1 :span 2) \"Header\"))
(flex :width (320) :gap (1 (12)) (item :flex 1 (box \"A\")))
`box' and `ebox' create `ebox-create' nodes. `row', `column',
@ -249,7 +238,6 @@ the child layout in a box using the rendered child layout as content."
('column (ebox--build-layout tag #'ebox-column items))
('flex (ebox--build-flex items))
('item (ebox--build-flex-item items))
('grid-item (ebox--build-grid-item items))
('spacer (ebox--build-spacer items))
('grid (ebox--build-grid items))
(_ (error "ebox-build: unknown DSL tag %S" tag)))))))

View File

@ -342,29 +342,6 @@ programmatic port and are therefore rejected here."
(error "ebox-grid-fr: FACTOR must be positive: %S" factor))
(list 'fr factor))
;;;###autoload
(defun ebox-grid-item (node &rest props)
"Return NODE with grid placement PROPS attached.
Supported properties are `:grid-column', `:grid-row',
`:grid-column-span', and `:grid-row-span'."
(unless (and (listp node) (plist-member node :ebox-type))
(error "ebox-grid-item: NODE is not an Ebox node: %S" node))
(let ((copy (copy-sequence node))
(declarations (ebox-style-compile-declarations props t)))
(while props
(let* ((key (pop props))
(value (progn
(unless props
(error "ebox-grid-item: missing value for %S" key))
(pop props))))
(unless (memq key ebox--grid-item-prop-keys)
(error "ebox-grid-item: unsupported property %S" key))
(plist-put copy key value)))
(plist-put copy :ebox-style-declarations
(append (plist-get copy :ebox-style-declarations)
declarations))
copy))
(defun ebox-grid--placement-part (value default)
"Return (START SPAN) parsed from placement VALUE and DEFAULT."
(cond
@ -895,7 +872,8 @@ Grid properties include `:grid-template-columns', `:grid-template-rows',
`:grid-auto-flow', `:gap', `:grid-row-gap', `:grid-column-gap',
`:justify-items', `:align-items', `:width', and `:height'. Track sizes use
Ebox pixel/line units, `auto', `(fr FACTOR)', or symbols such as `1fr'.
Children may carry `:grid-column' and `:grid-row' placement properties.
Children may carry `:grid-column', `:grid-row', `:grid-column-span', and
`:grid-row-span' placement properties directly.
`ebox-grid-fr' is a convenient constructor for fractional tracks."
(let* ((split (ebox-grid--split-attrs items))
(raw-props (car split))

View File

@ -4608,7 +4608,6 @@ through dirty-set and patch-set execution before falling back to root rerender."
ebox-grid
ebox-grid-fr
ebox-grid-layout-create
ebox-grid-item
ebox-host-ref-bounds
ebox-host-ref-position
ebox-native-build
@ -4693,7 +4692,7 @@ and are documented separately in the public API reference.")
;; :padding :margin :border :border-color :bgcolor
;; :text-align :wrap-mode :box-sizing :surface-properties :host-ref
;; ebox-build dsl
;; Compile ETML-style (box/row/column/flex/item/spacer ...) forms into
;; Compile ETML-style (box/row/column/flex/spacer ...) forms into
;; normal ebox layout nodes.
;;
;; ── Layout ────────────────────────────────────────────────────

View File

@ -100,6 +100,16 @@
(should-not (string-match-p "`etaf-[[:alnum:]-]+\\.el`" source))
(should-not (string-match-p "`etaf\\.el`" source)))))
(ert-deftest ebox-docs-do-not-present-removed-grid-item-api ()
"Public documentation must expose direct child Grid placement only."
(dolist (file '("docs/user/ebox-api-reference.en.md"
"docs/user/ebox-api-reference.zh.md"
"docs/user/ebox-user-guide.en.md"
"docs/user/ebox-user-guide.zh.md"))
(let ((source (ebox-docs-test--read file)))
(should-not (string-match-p "ebox-grid-item" source))
(should-not (string-match-p "`grid-item`" source)))))
(provide 'ebox-docs-contract-tests)
;;; ebox-docs-contract-tests.el ends here

View File

@ -161,12 +161,9 @@
(ebox-grid
:grid-template-columns '((20) (20))
:grid-template-rows '(1 1)
(ebox-grid-item (ebox-create :content "Header")
:grid-column '(1 :span 2))
(ebox-grid-item (ebox-create :content "Left")
:grid-row 2 :grid-column 1)
(ebox-grid-item (ebox-create :content "Right")
:grid-row 2 :grid-column 2)))))
(ebox-create :content "Header" :grid-column '(1 :span 2))
(ebox-create :content "Left" :grid-row 2 :grid-column 1)
(ebox-create :content "Right" :grid-row 2 :grid-column 2)))))
(should (string-match-p "Header" plain))
(should (string-match-p "Left.*Right" plain))))
@ -217,11 +214,11 @@
(should-error
(ebox-render
(ebox-grid
(ebox-grid-item (ebox-create :content "A") :grid-column-span 0))))
(ebox-create :content "A" :grid-column-span 0))))
(should-error
(ebox-render
(ebox-grid
(ebox-grid-item (ebox-create :content "A") :grid-column 0)))))
(ebox-create :content "A" :grid-column 0)))))
(ert-deftest ebox-grid-rejects-invalid-ecss-placement-before-layout ()
"ECSS rules should reject invalid Grid placement before layout runs."
@ -266,13 +263,12 @@
(should (string-match-p "A" (ebox-grid-test--plain node)))
(should (string-match-p "B" (ebox-grid-test--plain node)))))
(ert-deftest ebox-build-compiles-grid-item-placement ()
"The Ebox DSL should preserve grid-item placement metadata."
(ert-deftest ebox-build-compiles-direct-grid-placement ()
"The Ebox DSL should preserve direct child Box placement metadata."
(let ((node
(ebox-build
'(grid :grid-template-columns ((20) (20))
(grid-item :grid-column (1 :span 2)
(box :content "Header"))))))
(box :grid-column (1 :span 2) :content "Header")))))
(should (string-match-p "Header" (ebox-grid-test--plain node)))
(should (equal (plist-get (car (plist-get node :children)) :grid-column)
'(1 :span 2)))))

View File

@ -170,7 +170,6 @@
("ebox-flex-item" . "defun")
("ebox-flex" . "defun")
("ebox-grid-fr" . "defun")
("ebox-grid-item" . "defun")
("ebox-grid" . "defun")
("ebox-grid-layout-create" . "defun")
("ebox-normal-layout-create" . "defun")
@ -230,7 +229,6 @@
ebox-grid
ebox-grid-fr
ebox-grid-layout-create
ebox-grid-item
ebox-host-ref-bounds
ebox-host-ref-position
ebox-native-build