feat: add canonical Box layout axes

This commit is contained in:
Kinneyzhang 2026-08-26 16:10:40 +08:00
parent fcfbeb0368
commit 9f2cdfb299
2 changed files with 104 additions and 6 deletions

View File

@ -14,6 +14,12 @@
(declare-function ebox-create "ebox" (&rest plist)) (declare-function ebox-create "ebox" (&rest plist))
(defconst ebox-dsl--outer-values '(inline block)
"Valid outer participation values for the canonical Box DSL.")
(defconst ebox-dsl--layout-values '(normal row column flex grid)
"Valid child layout values for the canonical Box DSL.")
(defun ebox--dsl-unquote-value (value) (defun ebox--dsl-unquote-value (value)
"Unquote one static DSL property VALUE when it is quoted data." "Unquote one static DSL property VALUE when it is quoted data."
(if (and (consp value) (if (and (consp value)
@ -38,6 +44,22 @@ keyword properties are normalized."
(push item result)))) (push item result))))
(nreverse result))) (nreverse result)))
(defun ebox-dsl--axis-value (props property default allowed)
"Return PROPS PROPERTY or DEFAULT after validating it against ALLOWED."
(let ((value (if (plist-member props property)
(plist-get props property)
default)))
(unless (memq value allowed)
(error "ebox-build: invalid %S value %S" property value))
value))
(defun ebox-dsl--with-display (node outer layout)
"Return a shallow copy of NODE with canonical OUTER and LAYOUT display."
(let ((copy (copy-sequence node)))
(plist-put copy :display
(list outer (if (eq layout 'normal) 'flow layout)))
copy))
(defun ebox--build-children-layout (children) (defun ebox--build-children-layout (children)
"Build a vertical default layout from CHILDREN." "Build a vertical default layout from CHILDREN."
(setq children (delq nil children)) (setq children (delq nil children))
@ -134,6 +156,13 @@ The first value is string content. The second value is a lazy child node."
(ebox-dsl-build (car children)) (ebox-dsl-build (car children))
props))) props)))
(defun ebox--build-grid (items)
"Build a grid node from DSL ITEMS."
(let* ((split (ebox-grid--split-attrs items))
(props (car split))
(children (mapcar #'ebox-dsl-build (delq nil (cdr split)))))
(apply #'ebox-grid (append props children))))
(defun ebox--build-spacer (items) (defun ebox--build-spacer (items)
"Build a spacer node from DSL ITEMS." "Build a spacer node from DSL ITEMS."
(let* ((split (ebox--build-split-attrs items)) (let* ((split (ebox--build-split-attrs items))
@ -145,6 +174,33 @@ The first value is string content. The second value is a lazy child node."
(error "ebox-build: spacer cannot set :content")) (error "ebox-build: spacer cannot set :content"))
(apply #'ebox-spacer props))) (apply #'ebox-spacer props)))
(defun ebox--build-canonical-box (items)
"Build canonical Box ITEMS with orthogonal outer and layout axes."
(let* ((split (ebox--build-split-attrs items))
(props (car split))
(children (cdr split))
(outer (ebox-dsl--axis-value
props :outer 'block ebox-dsl--outer-values))
(layout (ebox-dsl--axis-value
props :layout 'normal ebox-dsl--layout-values))
(box-props (ebox--plist-remove-keys props '(:outer :layout)))
node)
(setq node
(pcase layout
('normal
(ebox--build-box (append box-props children)))
('row
(ebox--build-layout
'box #'ebox-row (append box-props children)))
('column
(ebox--build-layout
'box #'ebox-column (append box-props children)))
('flex
(ebox--build-flex (append box-props children)))
('grid
(ebox--build-grid (append box-props children)))))
(ebox-dsl--with-display node outer layout)))
(defun ebox-dsl-build (dsl) (defun ebox-dsl-build (dsl)
"Build an Ebox node from an ETML-style list DSL. "Build an Ebox node from an ETML-style list DSL.
@ -171,18 +227,15 @@ the child layout in a box using the rendered child layout as content."
(let ((tag (car dsl)) (let ((tag (car dsl))
(items (ebox--dsl-unquote-properties (cdr dsl)))) (items (ebox--dsl-unquote-properties (cdr dsl))))
(pcase tag (pcase tag
((or 'box 'ebox) (ebox--build-box items)) ('box (ebox--build-canonical-box items))
('ebox (ebox--build-box items))
('row (ebox--build-layout tag #'ebox-row items)) ('row (ebox--build-layout tag #'ebox-row items))
('column (ebox--build-layout tag #'ebox-column items)) ('column (ebox--build-layout tag #'ebox-column items))
('flex (ebox--build-flex items)) ('flex (ebox--build-flex items))
('item (ebox--build-flex-item items)) ('item (ebox--build-flex-item items))
('grid-item (ebox--build-grid-item items)) ('grid-item (ebox--build-grid-item items))
('spacer (ebox--build-spacer items)) ('spacer (ebox--build-spacer items))
('grid ('grid (ebox--build-grid items))
(let* ((split (ebox-grid--split-attrs items))
(props (car split))
(children (mapcar #'ebox-dsl-build (delq nil (cdr split)))))
(apply #'ebox-grid (append props children))))
(_ (error "ebox-build: unknown DSL tag %S" tag))))))) (_ (error "ebox-build: unknown DSL tag %S" tag)))))))
(provide 'ebox-dsl) (provide 'ebox-dsl)

View File

@ -358,6 +358,51 @@
(should (equal (ebox--computed-display column-node) '(block column))) (should (equal (ebox--computed-display column-node) '(block column)))
(should (equal (ebox--computed-display flex-node) '(block flex))))) (should (equal (ebox--computed-display flex-node) '(block flex)))))
(ert-deftest ebox-build-box-uses-orthogonal-outer-and-layout-axes ()
"Canonical Box DSL should select outer participation and child layout."
(let ((normal (ebox-build '(box :outer inline :layout normal "A")))
(row (ebox-build
'(box :outer inline :layout row
(box "A")
(box "B"))))
(column (ebox-build
'(box :outer block :layout column
(box "A")
(box "B")))))
(should (equal (ebox--computed-display normal) '(inline flow)))
(should (equal (ebox--computed-display row) '(inline row)))
(should (equal (ebox--computed-display column) '(block column)))
(should (string= (ebox-dsl-test--plain row) "AB"))
(should (string= (ebox-dsl-test--plain column) "A\nB"))))
(ert-deftest ebox-build-box-rejects-invalid-axis-values ()
"Canonical Box DSL should reject unknown outer and layout values."
(should-error (ebox-build '(box :outer floating "A")) :type 'error)
(should-error (ebox-build '(box :layout masonry "A")) :type 'error))
(ert-deftest ebox-build-box-layout-consumes-direct-flex-item-properties ()
"Canonical Flex Box should consume participation props from child Boxes."
(let* ((node (ebox-build
'(box :layout flex :width (300)
(box :flex-grow 1 :flex-basis (80) "A")
(box :flex-grow 2 :flex-basis (80) "B"))))
(children (plist-get node :children)))
(should (equal (ebox--computed-display node) '(block flex)))
(should (= (plist-get (car children) :flex-grow) 1))
(should (= (plist-get (cadr children) :flex-grow) 2))
(should (string-match-p "A" (ebox-dsl-test--plain node)))
(should (string-match-p "B" (ebox-dsl-test--plain node)))))
(ert-deftest ebox-build-box-layout-consumes-direct-grid-item-properties ()
"Canonical Grid Box should consume placement props from child Boxes."
(let* ((node (ebox-build
'(box :layout grid :grid-template-columns ((20) (20))
(box :grid-column (1 :span 2) "Header"))))
(child (car (plist-get node :children))))
(should (equal (ebox--computed-display node) '(block grid)))
(should (equal (plist-get child :grid-column) '(1 :span 2)))
(should (string-match-p "Header" (ebox-dsl-test--plain node)))))
(ert-deftest ebox-build-wrapper-child-layout-rerenders-on-runtime-viewport-change () (ert-deftest ebox-build-wrapper-child-layout-rerenders-on-runtime-viewport-change ()
"A DSL wrapper box should rerender child layout content on viewport resize." "A DSL wrapper box should rerender child layout content on viewport resize."
(let ((node (let ((ebox-viewport-width 96)) (let ((node (let ((ebox-viewport-width 96))