diff --git a/docs/user/ebox-api-reference.en.md b/docs/user/ebox-api-reference.en.md index 6ee33b3..1965694 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-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. | diff --git a/docs/user/ebox-api-reference.zh.md b/docs/user/ebox-api-reference.zh.md index 8ce6982..77276e3 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-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。 | diff --git a/ebox-canonical.el b/ebox-canonical.el index 634591a..68eb3f4 100644 --- a/ebox-canonical.el +++ b/ebox-canonical.el @@ -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")) diff --git a/ebox-flex.el b/ebox-flex.el index bc05c5e..9d66726 100644 --- a/ebox-flex.el +++ b/ebox-flex.el @@ -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. diff --git a/ebox-layout-config.el b/ebox-layout-config.el new file mode 100644 index 0000000..2099adb --- /dev/null +++ b/ebox-layout-config.el @@ -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 diff --git a/ebox-layout.el b/ebox-layout.el index 977e90c..1790c01 100644 --- a/ebox-layout.el +++ b/ebox-layout.el @@ -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) diff --git a/ebox.el b/ebox.el index 43e8c50..e30096b 100644 --- a/ebox.el +++ b/ebox.el @@ -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 diff --git a/tests/ebox-dsl-tests.el b/tests/ebox-dsl-tests.el index 9f0ceac..6a5be41 100644 --- a/tests/ebox-dsl-tests.el +++ b/tests/ebox-dsl-tests.el @@ -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)) diff --git a/tests/ebox-package-tests.el b/tests/ebox-package-tests.el index df337b4..39a71a3 100644 --- a/tests/ebox-package-tests.el +++ b/tests/ebox-package-tests.el @@ -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