feat: drive default FlexConfig through canonical Box

This commit is contained in:
Kinneyzhang 2026-08-26 19:25:27 +08:00
parent 53001719c4
commit e7dcb55d61
9 changed files with 270 additions and 53 deletions

View File

@ -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-box-create` | Framework-integration port for typed TextNode, Normal/Row/Column LayoutConfig, and BoxNode values. |
| `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-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. |

View File

@ -533,7 +533,7 @@ style API。
| 函数 | 用途 |
| --- | --- |
| `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-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-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。 |

View File

@ -11,16 +11,10 @@
(require 'cl-lib)
(require 'subr-x)
(require 'ebox-layout-config)
(require 'ebox-style)
(require 'ebox-node-factory)
(cl-defstruct
(ebox-layout-config
(:constructor ebox-layout-config--create))
"Canonical typed layout selection for one BoxNode."
kind
props)
(defun ebox-canonical--validate-plist (plist context)
"Validate PLIST shape for error CONTEXT and return PLIST."
(unless (proper-list-p plist)
@ -91,21 +85,6 @@
(error "%s does not accept %S"
context (plist-get property :name)))))
;;;###autoload
(defun ebox-normal-layout-create ()
"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)))
@ -132,7 +111,7 @@
"Return canonical BoxNode NODE's typed layout config."
(unless (ebox-box-node-p node)
(error "Expected canonical BoxNode, got %S" node))
(plist-get node :ebox-layout-config))
(ebox-layout-config--copy (plist-get node :ebox-layout-config)))
(defun ebox-box-node-children (node)
"Return canonical BoxNode NODE's retained canonical children."
@ -188,6 +167,7 @@ child. Row and Column layouts accept any number of children."
(unless (ebox-layout-config-p layout)
(error "Ebox Box :layout must be a typed layout config: %S"
layout))
(setq layout (ebox-layout-config--copy layout))
(unless (memq outer '(inline block))
(error "Ebox Box :outer must be inline or block: %S" outer))
(unless (proper-list-p children)
@ -198,12 +178,9 @@ 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))
(unless (memq (ebox-layout-config-kind layout) '(normal row column flex))
(error "Ebox Box Layout is not implemented: %S"
(ebox-layout-config-kind layout)))
(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"))

View File

@ -9,6 +9,7 @@
(require 'cl-lib)
(require 'subr-x)
(require 'ebox-layout-config)
(require 'ebox-layout)
(declare-function ebox--flex-fragment-allocation-key
@ -266,8 +267,8 @@ ALLOWED-KEYWORDS is the CSS keyword set valid for the property being parsed."
(cons (nth 0 gap) (nth 1 gap)))
(t (cons gap gap))))
(defun ebox--flex-normalize-container-props (props)
"Normalize flex container PROPS to explicit fields."
(defun ebox--flex-normalize-config-props (props)
"Normalize FlexConfig PROPS to explicit layout-owned fields."
(let* ((flow (plist-get props :flex-flow))
(flow-values (if (listp flow) flow (list flow)))
(flow-direction (cl-find-if
@ -295,9 +296,28 @@ ALLOWED-KEYWORDS is the CSS keyword set valid for the property being parsed."
(car gap-pair))
:column-gap (if (plist-member props :column-gap)
(plist-get props :column-gap)
(cdr gap-pair))
:width (plist-get props :width)
:height (plist-get props :height))))
(cdr gap-pair)))))
(defun ebox--flex-normalize-container-props (props)
"Normalize legacy flex container PROPS to config plus frame constraints."
(append (ebox--flex-normalize-config-props props)
(list :width (plist-get props :width)
:height (plist-get props :height))))
(defconst ebox--flex-default-config-props
(ebox--flex-normalize-config-props nil)
"Validated property set for the first default FlexConfig slice.")
(defun ebox-flex-layout-config-props-p (props)
"Return non-nil when PROPS are the validated default FlexConfig."
(equal props ebox--flex-default-config-props))
;;;###autoload
(defun ebox-flex-layout-create ()
"Return the canonical Flex layout config with computed defaults."
(ebox-layout-config--create
:kind 'flex
:props (copy-sequence ebox--flex-default-config-props)))
(defun ebox--flex-axis (props)
"Return main axis for normalized flex PROPS."
@ -1557,14 +1577,10 @@ its child."
(puthash region-id box ebox--region-box-table)))
rendered))
(defun ebox--render-flex (node)
"Render flex NODE to a propertized string."
(let* ((props (ebox--flex-container-content-props
(plist-get node :props)
(plist-get node :raw-props)
(plist-get node :box)))
(defun ebox--render-flex-children (config constraints children)
"Render Flex CONFIG under frame CONSTRAINTS over flat CHILDREN."
(let* ((props (append config constraints))
(axis (ebox--flex-axis props))
(children (ebox-tree-layout-children node))
(main-size (if (eq axis 'row)
(ebox--flex-horizontal-value (plist-get props :width))
(ebox--flex-line-value (plist-get props :height))))
@ -1587,13 +1603,39 @@ its child."
items main-size main-gap (plist-get props :flex-wrap))))
(when (eq (plist-get props :flex-wrap) 'wrap-reverse)
(setq lines (nreverse lines)))
(if (eq axis 'row)
(ebox--render-flex-row lines props main-size cross-size
main-gap cross-gap)
(ebox--render-flex-column lines props main-size cross-size
main-gap cross-gap))))
(defun ebox--flex-box-constraints (box)
"Project BOX content dimensions to private FlexConstraints."
(let ((width (ebox--wrapper-content-viewport-pixel box))
(height (ebox--resolve-size-content-height
box (ebox-get box :height) nil)))
(list :width (and width (list width))
:height height)))
(defun ebox--render-flex-box-children (box props children)
"Render canonical BOX Flex PROPS over flat CHILDREN."
(ebox--render-flex-children
props
(ebox--flex-box-constraints box)
children))
(defun ebox--render-flex (node)
"Render legacy flex NODE to a propertized string."
(let ((props (ebox--flex-container-content-props
(plist-get node :props)
(plist-get node :raw-props)
(plist-get node :box))))
(ebox--render-flex-box
node
(if (eq axis 'row)
(ebox--render-flex-row lines props main-size cross-size
main-gap cross-gap)
(ebox--render-flex-column lines props main-size cross-size
main-gap cross-gap)))))
(ebox--render-flex-children
(ebox--plist-remove-keys props '(:width :height))
(ebox--plist-keep-keys props '(:width :height))
(ebox-tree-layout-children node)))))
(defun ebox--flex-prewarm-sized-item (node _target-width)
"Prepare one fixed-cross flex item phase and yield to the event loop.

64
ebox-layout-config.el Normal file
View File

@ -0,0 +1,64 @@
;;; ebox-layout-config.el --- Typed Box layout selection -*- lexical-binding: t; -*-
;;; Commentary:
;; Owns the small backend-neutral value that selects one Box child-layout
;; algorithm. Layout modules construct validated variants; the value itself
;; carries no runtime identity, children, measurement, or publication state.
;;; Code:
(require 'cl-lib)
(declare-function ebox-flex-layout-config-props-p
"ebox-flex" (props))
(cl-defstruct
(ebox-layout-config
(:constructor ebox-layout-config--create))
"Canonical typed layout selection for one BoxNode."
kind
props)
(defun ebox-layout-config-validate (config)
"Return CONFIG after revalidating its variant-owned properties."
(unless (ebox-layout-config-p config)
(error "Expected an Ebox LayoutConfig: %S" config))
(let* ((kind (ebox-layout-config-kind config))
(props (ebox-layout-config-props config))
(valid-p
(pcase kind
((or 'normal 'row 'column) (null props))
('flex
(and (fboundp 'ebox-flex-layout-config-props-p)
(ebox-flex-layout-config-props-p props)))
(_ nil))))
(unless valid-p
(error "Invalid %S LayoutConfig properties: %S"
kind props)))
config)
(defun ebox-layout-config--copy (config)
"Return a detached copy of validated CONFIG."
(ebox-layout-config-validate config)
(ebox-layout-config--create
:kind (ebox-layout-config-kind config)
:props (copy-tree (ebox-layout-config-props config))))
;;;###autoload
(defun ebox-normal-layout-create ()
"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))
(provide 'ebox-layout-config)
;;; ebox-layout-config.el ends here

View File

@ -9,6 +9,7 @@
(require 'cl-lib)
(require 'seq)
(require 'subr-x)
(require 'ebox-layout-config)
(require 'ebox-tree)
(require 'ebox-measure)
(require 'ebox-fragment)
@ -17,7 +18,8 @@
(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-flex-box-children
"ebox-flex" (box props children))
(declare-function ebox--render-with-cache
"ebox-incremental" (node &optional force cache-probe))
(declare-function ebox-create "ebox" (&rest plist))
@ -957,9 +959,15 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
('column
(ebox--call-with-box-content-context
box (lambda () (ebox--render-column-children children))))
('flex
(ebox--render-flex-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)))
box content (memq kind '(row column flex)))
content))
(defun ebox--box-content (box)

View File

@ -23,7 +23,8 @@
"Directory containing the active Ebox Lisp sources.")
(defconst ebox--compile-sources
'("ebox-cache.el" "ebox-style.el" "ebox-node-factory.el"
'("ebox-cache.el" "ebox-style.el" "ebox-layout-config.el"
"ebox-node-factory.el"
"ebox-child-range.el" "ebox-tree.el" "ebox-measure.el"
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
"ebox-flex.el" "ebox-grid.el" "ebox-canonical.el"
@ -41,6 +42,7 @@
(require 'subr-x)
(require 'ebox-cache)
(require 'ebox-style)
(require 'ebox-layout-config)
(require 'ebox-node-factory)
(require 'ebox-child-range)
(require 'ebox-tree)
@ -4601,6 +4603,7 @@ through dirty-set and patch-set execution before falling back to root rerender."
ebox-display-buffer
ebox-display-signature
ebox-flex
ebox-flex-layout-create
ebox-flex-item
ebox-grid
ebox-grid-fr

View File

@ -216,6 +216,123 @@
(should (equal (ebox--computed-display box) '(block column)))
(should (string= (ebox-dsl-test--plain box) "A\nB"))))
(ert-deftest ebox-canonical-flex-box-uses-one-box-identity-and-child-list ()
"A FlexConfig should select the Box algorithm without a Flex runtime node."
(let* ((left (ebox-text-create :value "A"))
(right (ebox-text-create :value "B"))
(layout (ebox-flex-layout-create))
(box (ebox-box-create :layout layout
:children (list left right)
:width '(120)))
(rendered (ebox-render box)))
(should
(equal (ebox-layout-config-props layout)
'(:flex-direction row :flex-wrap nowrap
:justify-content flex-start :align-items stretch
:align-content stretch :row-gap nil :column-gap nil)))
(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)) 'flex))
(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) 'flex)))
0))
(should-not (plist-member box :ebox-content-node))
(should (equal (ebox--computed-display box) '(block flex)))
(should (string= (string-trim-right
(substring-no-properties rendered))
"AB"))
(should (= (ebox--string-pixel-width rendered) 120))))
(ert-deftest ebox-canonical-flex-revalidates-the-typed-config-boundary ()
"Mutated or extra FlexConfig properties should fail before rendering."
(let ((invalid-direction (ebox-flex-layout-create))
(extra-frame-prop (ebox-flex-layout-create))
(cross-kind-props (ebox-flex-layout-create)))
(setf (ebox-layout-config-props invalid-direction)
'(:flex-direction bogus :flex-wrap nowrap
:justify-content flex-start :align-items stretch
:align-content stretch :row-gap nil :column-gap nil))
(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) 'row)
(should-error
(ebox-box-create :layout invalid-direction :children nil)
:type 'error)
(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-box-detaches-validated-layout-config ()
"Caller and accessor mutations should not alter a constructed Box."
(let* ((source-layout (ebox-flex-layout-create))
(box (ebox-box-create
:layout source-layout
:children (list (ebox-text-create :value "A")
(ebox-text-create :value "B"))))
(exposed-layout (ebox-box-node-layout box)))
(should-not (fboundp 'ebox-layout-config-validator))
(setf (ebox-layout-config-props source-layout) '(:flex-direction bogus))
(setf (ebox-layout-config-props exposed-layout) '(:width 999))
(should (string= (string-trim-right
(substring-no-properties (ebox-render box)))
"AB"))
(should (equal (ebox-layout-config-props (ebox-box-node-layout box))
ebox--flex-default-config-props))))
(ert-deftest ebox-canonical-flex-projects-one-box-frame-constraint ()
"Typed and legacy Flex should derive the same content size from BoxFrame."
(dolist (sizing '(border-box content-box))
(let* ((typed
(ebox-box-create
:layout (ebox-flex-layout-create)
:children (list (ebox-text-create :value "A")
(ebox-text-create :value "B"))
:width '(140) :padding '(0 (10)) :box-sizing sizing))
(legacy
(ebox-flex
: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=
(string-trim-right (substring-no-properties typed-rendered))
(string-trim-right (substring-no-properties legacy-rendered))))
(should (= (ebox--string-pixel-width typed-rendered)
(ebox--string-pixel-width 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"))
(right-text (ebox-text-create :value "B"))
(left (ebox-box-create :layout (ebox-normal-layout-create)
:children (list left-text)
:order 2))
(right (ebox-box-create :layout (ebox-normal-layout-create)
:children (list right-text)
:order 1))
(box (ebox-box-create :layout (ebox-flex-layout-create)
:children (list left right)))
(rendered (substring-no-properties (ebox-render box))))
(should (string= (string-trim-right rendered) "BA"))
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 5))
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 5))
(should
(= (ebox-dsl-test--tree-count
box (lambda (node) (eq (plist-get node :ebox-type) 'flex-item)))
0))))
(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"))
@ -234,10 +351,11 @@
(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."
(ert-deftest ebox-canonical-empty-layout-boxes-have-no-runtime-child ()
"Empty layout Boxes should not synthesize layout identities."
(dolist (layout (list (ebox-row-layout-create)
(ebox-column-layout-create)))
(ebox-column-layout-create)
(ebox-flex-layout-create)))
(let ((box (ebox-box-create :layout layout :children nil)))
(should (ebox-box-node-p box))
(should-not (ebox-tree-node-children box))

View File

@ -11,6 +11,7 @@
(defconst ebox-test--autoload-source-files
'("ebox.el"
"ebox-layout-config.el"
"ebox-node-factory.el"
"ebox-measure.el"
"ebox-layout.el"
@ -51,6 +52,7 @@
"The public ebox facade should load all internal foundation modules."
(require 'ebox)
(dolist (feature '(ebox-cache ebox-style ebox-tree ebox-measure
ebox-layout-config
ebox-node-factory
ebox-fragment ebox-layout ebox-flex ebox-grid
ebox-canonical
@ -91,7 +93,8 @@
(should (commandp 'ebox-byte-compile))
(should
(equal ebox--compile-sources
'("ebox-cache.el" "ebox-style.el" "ebox-node-factory.el"
'("ebox-cache.el" "ebox-style.el" "ebox-layout-config.el"
"ebox-node-factory.el"
"ebox-child-range.el"
"ebox-tree.el" "ebox-measure.el"
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
@ -172,6 +175,7 @@
("ebox-normal-layout-create" . "defun")
("ebox-row-layout-create" . "defun")
("ebox-column-layout-create" . "defun")
("ebox-flex-layout-create" . "defun")
("ebox-text-create" . "defun")
("ebox-box-create" . "defun")
("ebox-build" . "defun")
@ -220,6 +224,7 @@
ebox-display-buffer
ebox-display-signature
ebox-flex
ebox-flex-layout-create
ebox-flex-item
ebox-grid
ebox-grid-fr