feat: complete typed Normal Row and Column layout core

This commit is contained in:
Kinneyzhang 2026-08-26 21:33:32 +08:00
parent 6d9f2a4d82
commit e5276d1cac
5 changed files with 372 additions and 66 deletions

View File

@ -141,6 +141,7 @@
(plist-put node :ebox-kind 'text) (plist-put node :ebox-kind 'text)
(plist-put node :ebox-text-value value) (plist-put node :ebox-text-value value)
(plist-put node :ebox-source-handle source-handle) (plist-put node :ebox-source-handle source-handle)
(plist-put node :display '(inline flow))
node))) node)))
;;;###autoload ;;;###autoload
@ -148,8 +149,8 @@
"Create a canonical BoxNode from evaluated PLIST. "Create a canonical BoxNode from evaluated PLIST.
`:layout' must occur exactly once as an `ebox-layout-config'. `:layout' must occur exactly once as an `ebox-layout-config'.
`:children' is a list of canonical nodes. Normal layout accepts zero or one `:children' is a list of canonical nodes. Every Layout accepts zero or more
child. Row and Column layouts accept any number of children." children according to its own formatting algorithm."
(ebox-canonical--validate-plist plist "ebox-box-create") (ebox-canonical--validate-plist plist "ebox-box-create")
(let* ((layout (ebox-canonical--required-field (let* ((layout (ebox-canonical--required-field
plist :layout "Ebox Box")) plist :layout "Ebox Box"))
@ -182,9 +183,6 @@ child. Row and Column layouts accept any number of children."
'(normal row column flex grid)) '(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)
(> (length children) 1))
(error "Ebox Box Normal layout currently accepts at most one child"))
(ebox-canonical--assert-property-role (ebox-canonical--assert-property-role
props #'ebox-canonical--box-frame-property-p "Ebox Box") props #'ebox-canonical--box-frame-property-p "Ebox Box")
(let ((node (apply #'ebox-create props))) (let ((node (apply #'ebox-create props)))

View File

@ -8,6 +8,7 @@
;;; Code: ;;; Code:
(require 'cl-lib) (require 'cl-lib)
(require 'seq)
(declare-function ebox-flex-layout-config-props-p (declare-function ebox-flex-layout-config-props-p
"ebox-flex" (props)) "ebox-flex" (props))
@ -21,6 +22,41 @@
kind kind
props) props)
(defconst ebox-layout-config--simple-cross-align-values
'(start center end stretch)
"Canonical Row/Column cross-axis alignment values.")
(defun ebox-layout-config--simple-axis-props-p (props)
"Return non-nil when PROPS is canonical Row/Column config data."
(and (equal (mapcar #'car (seq-partition props 2))
'(:item-gap :cross-align))
(let ((gap (plist-get props :item-gap))
(align (plist-get props :cross-align)))
(and (integerp gap) (>= gap 0)
(memq align ebox-layout-config--simple-cross-align-values)))))
(defun ebox-layout-config--simple-axis-create (kind plist)
"Return validated simple-axis KIND LayoutConfig from PLIST."
(unless (and (proper-list-p plist) (zerop (% (length plist) 2)))
(error "Ebox %S LayoutConfig requires an even plist: %S" kind plist))
(let ((seen (make-hash-table :test #'eq)))
(cl-loop for (key _value) on plist by #'cddr
unless (memq key '(:item-gap :cross-align))
do (error "Ebox %S LayoutConfig does not accept %S" kind key)
do (when (gethash key seen)
(error "Duplicate Ebox %S LayoutConfig property: %S" kind key))
do (puthash key t seen)))
(let ((props
(list :item-gap (if (plist-member plist :item-gap)
(plist-get plist :item-gap)
0)
:cross-align (if (plist-member plist :cross-align)
(plist-get plist :cross-align)
'stretch))))
(unless (ebox-layout-config--simple-axis-props-p props)
(error "Invalid Ebox %S LayoutConfig properties: %S" kind props))
(ebox-layout-config--create :kind kind :props props)))
(defun ebox-layout-config-validate (config) (defun ebox-layout-config-validate (config)
"Return CONFIG after revalidating its variant-owned properties." "Return CONFIG after revalidating its variant-owned properties."
(unless (ebox-layout-config-p config) (unless (ebox-layout-config-p config)
@ -29,7 +65,9 @@
(props (ebox-layout-config-props config)) (props (ebox-layout-config-props config))
(valid-p (valid-p
(pcase kind (pcase kind
((or 'normal 'row 'column) (null props)) ('normal (null props))
((or 'row 'column)
(ebox-layout-config--simple-axis-props-p props))
('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)))
@ -55,14 +93,14 @@
(ebox-layout-config--create :kind 'normal :props nil)) (ebox-layout-config--create :kind 'normal :props nil))
;;;###autoload ;;;###autoload
(defun ebox-row-layout-create () (defun ebox-row-layout-create (&rest plist)
"Return the canonical Row layout config." "Return canonical RowConfig from evaluated PLIST."
(ebox-layout-config--create :kind 'row :props nil)) (ebox-layout-config--simple-axis-create 'row plist))
;;;###autoload ;;;###autoload
(defun ebox-column-layout-create () (defun ebox-column-layout-create (&rest plist)
"Return the canonical Column layout config." "Return canonical ColumnConfig from evaluated PLIST."
(ebox-layout-config--create :kind 'column :props nil)) (ebox-layout-config--simple-axis-create 'column plist))
(provide 'ebox-layout-config) (provide 'ebox-layout-config)

View File

@ -942,6 +942,142 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
(not (eq (and (listp node) (plist-get node :ebox-type)) 'box))) (not (eq (and (listp node) (plist-get node :ebox-type)) 'box)))
content)) content))
(defun ebox--normal-text-units (string)
"Return word/space/break units for inline Text STRING."
(let ((lines (ebox-string-lines string)) units)
(cl-loop for line in lines
for line-tail on lines
do (let ((position 0)
(limit (length line)))
(while (< position limit)
(let* ((space-p
(not (null
(memq (aref line position) '(?\s ?\t)))))
(end position))
(while (and (< end limit)
(eq (not (null
(memq (aref line end)
'(?\s ?\t))))
space-p))
(setq end (1+ end)))
(let ((part (substring line position end)))
(push (list :kind (if space-p 'space 'word)
:rendered part
:width (ebox--string-pixel-width part))
units))
(setq position end))))
unless (null (cdr line-tail))
do (push '(:kind break) units))
(nreverse units)))
(defun ebox--render-normal-box-children (box children)
"Render canonical CHILDREN without flattening inline Box boundaries."
(let* ((resolved-width (ebox--wrapper-content-viewport-pixel box))
(content-width (and resolved-width (> resolved-width 0)
resolved-width))
(line-width 0)
pending-empty-line
line-parts segments)
(cl-labels
((flush-line
()
(when line-parts
(push (apply #'ebox--lines-concat-horizontal
(nreverse line-parts))
segments)
(setq line-parts nil line-width 0)))
(append-atomic
(rendered width)
(when (and content-width line-parts
(> (+ line-width width) content-width))
(flush-line))
(push rendered line-parts)
(setq line-width (+ line-width width)
pending-empty-line nil))
(append-word
(rendered width)
(if (or (null content-width) (<= width content-width))
(append-atomic rendered width)
(let ((parts (ebox--wrap-line rendered content-width 'char)))
(cl-loop for part in parts
for tail on parts
do (append-atomic
part (ebox--string-pixel-width part))
unless (null (cdr tail)) do (flush-line)))))
(append-display-space
(rendered width)
(let ((remaining width)
(properties
(cl-loop for (key value) on (text-properties-at 0 rendered)
by #'cddr
unless (eq key 'display)
append (list key value))))
(while (> remaining 0)
(let* ((chunk (min remaining content-width))
(piece (ebox-pixel-space chunk)))
(when properties
(add-text-properties 0 (length piece) properties piece))
(append-atomic piece chunk)
(setq remaining (- remaining chunk))
(when (> remaining 0) (flush-line))))))
(force-break
()
(if line-parts
(flush-line)
(push "" segments))
(setq pending-empty-line t))
(append-unit
(unit)
(pcase (plist-get unit :kind)
('break (force-break))
('space
(let ((width (plist-get unit :width)))
(cond
((and content-width (> width content-width))
(let ((rendered (plist-get unit :rendered)))
(if (and (= (length rendered) 1)
(get-text-property 0 'display rendered))
(append-display-space rendered width)
(append-word rendered width))))
((null line-parts)
(append-atomic (plist-get unit :rendered) width))
((and content-width (> (+ line-width width) content-width))
(flush-line))
(t (append-atomic (plist-get unit :rendered) width)))))
('word
(append-word (plist-get unit :rendered)
(plist-get unit :width)))
('box
(append-atomic (plist-get unit :rendered)
(plist-get unit :width)))))
(render-child
(child)
(let ((inline-p (eq (ebox-tree-display-outer child) 'inline))
(rendered
(let ((ebox--inline-auto-width-intrinsic-p
(eq (ebox-tree-display-outer child) 'inline)))
(ebox--render-with-cache child))))
(if (not inline-p)
(progn
(flush-line)
(setq pending-empty-line nil)
(push rendered segments))
(if (eq (plist-get child :ebox-kind) 'text)
(dolist (unit (ebox--normal-text-units rendered))
(append-unit unit))
(append-unit
(list :kind 'box :rendered rendered
:width (ebox--string-max-pixel-width rendered))))))))
(ebox--call-with-box-content-context
box (lambda () (dolist (child children) (render-child child))))
(when pending-empty-line
(push "" segments)
(setq pending-empty-line nil))
(flush-line))
(if segments
(apply #'ebox--lines-stack-vertical (nreverse segments))
"")))
(defun ebox--render-box-layout-children (box) (defun ebox--render-box-layout-children (box)
"Render canonical BOX children through its typed LayoutConfig." "Render canonical BOX children through its typed LayoutConfig."
(let* ((kind (ebox--box-layout-kind box)) (let* ((kind (ebox--box-layout-kind box))
@ -952,15 +1088,21 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
(lambda () (lambda ()
(pcase kind (pcase kind
('normal ('normal
(if children (ebox--render-normal-box-children box children))
(ebox--render-node-as-box-content (car children) box)
""))
('row ('row
(ebox--call-with-box-content-context (ebox--call-with-box-content-context
box (lambda () (ebox--render-row-children children)))) box (lambda ()
(ebox--render-row-children
children
(ebox-layout-config-props
(plist-get box :ebox-layout-config))))))
('column ('column
(ebox--call-with-box-content-context (ebox--call-with-box-content-context
box (lambda () (ebox--render-column-children children)))) box (lambda ()
(ebox--render-column-children
children
(ebox-layout-config-props
(plist-get box :ebox-layout-config))))))
('flex ('flex
(ebox--render-flex-box-children (ebox--render-flex-box-children
box box
@ -975,7 +1117,8 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
children)) 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 grid))) box content (or (memq kind '(row column flex grid))
(consp (cdr children))))
content)) content))
(defun ebox--box-content (box) (defun ebox--box-content (box)
@ -1769,13 +1912,19 @@ A sole child Range keeps a material row parent so the Range stays addressable."
:display '(block row) :display '(block row)
:children nodes)))) :children nodes))))
(defun ebox--render-row-children (children) (defun ebox--render-row-children (children &optional config)
"Render flat row CHILDREN to one multi-line string. "Render flat row CHILDREN with optional typed Row CONFIG.
Auto-width children are rendered at intrinsic inline size while explicit Auto-width children are rendered at intrinsic inline size while explicit
viewport/definite widths keep their containing-block semantics. Shorter viewport/definite widths keep their containing-block semantics. Shorter
children are padded with blank lines so all reach the same height before children are padded with blank lines so all reach the same height before
horizontal concatenation." horizontal concatenation."
(let* ((rendered (let* ((gap (or (plist-get config :item-gap) 0))
(cross-align (or (plist-get config :cross-align) 'stretch))
(vertical-align (pcase cross-align
('center 'center)
('end 'bottom)
(_ 'top)))
(rendered
(mapcar (lambda (child) (mapcar (lambda (child)
;; A row owns the horizontal concatenation. Its auto ;; A row owns the horizontal concatenation. Its auto
;; width children must therefore measure intrinsically; ;; width children must therefore measure intrinsically;
@ -1787,13 +1936,17 @@ horizontal concatenation."
(max-h (if rendered (max-h (if rendered
(apply #'max (mapcar #'ebox-string-height rendered)) (apply #'max (mapcar #'ebox-string-height rendered))
0)) 0))
(line-lists pieces)
(mapcar (lambda (string) (cl-loop for string in rendered
(ebox-string-lines for tail on rendered
(ebox--height-pad string max-h))) do (push (ebox--lines-align-vertical
rendered))) string max-h vertical-align)
(if line-lists pieces)
(ebox-lines-join (apply #'cl-mapcar #'concat line-lists)) unless (null (cdr tail))
do (when (> gap 0)
(push (ebox--pixel-blank gap max-h) pieces)))
(if pieces
(apply #'ebox--lines-concat-horizontal (nreverse pieces))
""))) "")))
(defun ebox--render-concat (node) (defun ebox--render-concat (node)
@ -3145,8 +3298,8 @@ only used for incomplete lazy prefixes."
formatted-lines formatted-lines
(seq-take formatted-lines target-lines)))))))))) (seq-take formatted-lines target-lines))))))))))
(defun ebox--render-column-children (children) (defun ebox--render-column-children (children &optional config)
"Render flat column CHILDREN to one multi-line string. "Render flat column CHILDREN with optional typed Column CONFIG.
Stack leaves are rendered independently. When a containing-block viewport is Stack leaves are rendered independently. When a containing-block viewport is
bound, narrower rows are padded to that available width, while wider definite bound, narrower rows are padded to that available width, while wider definite
@ -3155,7 +3308,13 @@ intrinsic measurement, descendants still receive the containing block but the
stack itself uses its natural max child width. A stack used as an intrinsic stack itself uses its natural max child width. A stack used as an intrinsic
inline child follows the same natural-width rule and does not consume the inline child follows the same natural-width rule and does not consume the
ambient horizontal viewport." ambient horizontal viewport."
(let* ((rendered-items (let* ((gap (or (plist-get config :item-gap) 0))
(cross-align (or (plist-get config :cross-align) 'stretch))
(horizontal-align (pcase cross-align
('center 'center)
('end 'right)
(_ 'left)))
(rendered-items
(mapcar (lambda (leaf) (mapcar (lambda (leaf)
(let ((rendered (ebox--render-with-cache leaf))) (let ((rendered (ebox--render-with-cache leaf)))
(cons rendered (cons rendered
@ -3168,15 +3327,23 @@ ambient horizontal viewport."
ebox--inline-auto-width-intrinsic-p) ebox--inline-auto-width-intrinsic-p)
max-w max-w
(or (ebox--viewport-pixel-width nil) max-w)))) (or (ebox--viewport-pixel-width nil) max-w))))
(let ((rendered (let (pieces)
(apply #'ebox--lines-stack-vertical (cl-loop for item in rendered-items
(mapcar (lambda (item) for tail on rendered-items
(let ((rendered (car item)) do (push (let ((rendered (car item))
(width (cdr item))) (width (cdr item)))
(if (< width target-w) (if (< width target-w)
(ebox--width-pad rendered (- target-w width)) (ebox--lines-justify
rendered))) rendered target-w horizontal-align)
rendered-items)))) rendered))
pieces)
unless (null (cdr tail))
do (when (> gap 0)
(push (ebox--pixel-blank target-w gap) pieces)))
(let ((rendered
(if pieces
(apply #'ebox--lines-stack-vertical (nreverse pieces))
"")))
;; When no child overflows the containing block, stack padding proves ;; When no child overflows the containing block, stack padding proves
;; every output line already has the exact target width. A wrapping box ;; every output line already has the exact target width. A wrapping box
;; can then preserve this preformatted content without measuring every ;; can then preserve this preformatted content without measuring every
@ -3184,7 +3351,7 @@ ambient horizontal viewport."
;; and therefore must not publish uniform-width metadata. ;; and therefore must not publish uniform-width metadata.
(if (<= max-w target-w) (if (<= max-w target-w)
(ebox--record-rendered-uniform-width rendered target-w) (ebox--record-rendered-uniform-width rendered target-w)
rendered)))) rendered)))))
(defun ebox--render-stack (node) (defun ebox--render-stack (node)
"Render a legacy stack NODE to a string." "Render a legacy stack NODE to a string."

View File

@ -1312,13 +1312,20 @@ SNAPSHOT reuses a previously detached ECSS values snapshot when supplied."
(defun ebox-style--apply-display-axes (node style snapshot) (defun ebox-style--apply-display-axes (node style snapshot)
"Apply computed display and public axes from STYLE SNAPSHOT to NODE." "Apply computed display and public axes from STYLE SNAPSHOT to NODE."
(when (ebox-style--snapshot-active-p snapshot :display) (let* ((display-specified-p
(plist-put node :display (ebox-style--specified-property-p style 'ebox/display))
(ebox-style-computed-value style :display))) (outer-specified-p
(let* ((outer-specified-p
(ebox-style--specified-property-p style 'ebox/outer)) (ebox-style--specified-property-p style 'ebox/outer))
(layout-specified-p (layout-specified-p
(ebox-style--specified-property-p style 'ebox/layout))) (ebox-style--specified-property-p style 'ebox/layout)))
(if (eq (plist-get node :ebox-kind) 'text)
(progn
(when (or display-specified-p outer-specified-p layout-specified-p)
(error "Ebox Text is fixed inline and rejects display axes"))
(plist-put node :display '(inline flow)))
(when (ebox-style--snapshot-active-p snapshot :display)
(plist-put node :display
(ebox-style-computed-value style :display)))
(when (or outer-specified-p layout-specified-p) (when (or outer-specified-p layout-specified-p)
(let* ((display (or (plist-get node :display) '(block flow))) (let* ((display (or (plist-get node :display) '(block flow)))
(outer (if outer-specified-p (outer (if outer-specified-p
@ -1328,7 +1335,7 @@ SNAPSHOT reuses a previously detached ECSS values snapshot when supplied."
(ebox-style-computed-value style :layout) (ebox-style-computed-value style :layout)
(cadr display)))) (cadr display))))
(plist-put node :display (plist-put node :display
(list outer (if (eq layout 'normal) 'flow layout)))))) (list outer (if (eq layout 'normal) 'flow layout)))))))
node) node)
(defun ebox-style-apply-computed (node style) (defun ebox-style-apply-computed (node style)

View File

@ -164,32 +164,104 @@
(ebox-text-create :value "A" :source-handle 'a :source-handle 'b) (ebox-text-create :value "A" :source-handle 'a :source-handle 'b)
:type 'error)) :type 'error))
(ert-deftest ebox-canonical-normal-box-renders-one-typed-text-child () (ert-deftest ebox-canonical-normal-box-renders-inline-runs-and-blocks ()
"A canonical BoxNode should retain typed Layout and child source facts." "Normal should retain typed children and honor inline/block boundaries."
(let* ((text (ebox-text-create :value "A" :source-handle 'source-text)) (let* ((text (ebox-text-create :value "A" :source-handle 'source-text))
(second-text (ebox-text-create :value "B"))
(inline
(ebox-box-create
:layout (ebox-normal-layout-create) :outer 'inline
:width '(100)
:children (list (ebox-text-create :value "A B"))))
(block
(ebox-box-create
:layout (ebox-normal-layout-create)
:children (list (ebox-text-create :value "C"))))
(tail (ebox-text-create :value "D"))
(layout (ebox-normal-layout-create)) (layout (ebox-normal-layout-create))
(box (ebox-box-create :layout layout (box (ebox-box-create :layout layout
:children (list text) :children (list text second-text inline block tail)
:source-handle 'source-box))) :source-handle 'source-box
:width '(50) :overflow 'visible))
(rendered (ebox-render box))
(rendered-lines (ebox-string-lines rendered))
(lines (mapcar (lambda (line)
(string-trim-right
(substring-no-properties line)))
rendered-lines)))
(should (ebox-box-node-p box)) (should (ebox-box-node-p box))
(should-not (ebox-text-node-p box)) (should-not (ebox-text-node-p box))
(should (eq (ebox-node-source-handle box) 'source-box)) (should (eq (ebox-node-source-handle box) 'source-box))
(should (eq (ebox-layout-config-kind (should (eq (ebox-layout-config-kind
(ebox-box-node-layout box)) (ebox-box-node-layout box))
'normal)) 'normal))
(should (equal (ebox-box-node-children box) (list text))) (should (equal (ebox-box-node-children box)
(should (string= (ebox-dsl-test--plain box) "A")))) (list text second-text inline block tail)))
(should (equal (ebox--computed-display text) '(inline flow)))
(should (equal lines '("AB" "A B" "C" "D")))
(should (>= (ebox--string-pixel-width (nth 1 rendered-lines)) 100))
(let ((ebox-style-stylesheet (ecss-stylesheet-create)))
(plist-put text :class "fixed-inline-text")
(ebox-style-add-rule ".fixed-inline-text" (list :outer 'block))
(should-error (ebox-render box) :type 'error))
(cl-labels
((normal-lines
(value &optional width)
(mapcar #'substring-no-properties
(ebox-string-lines
(ebox-render
(apply #'ebox-box-create
:layout (ebox-normal-layout-create)
:children (list (ebox-text-create :value value))
(when width
(list :width (list width)
:overflow 'visible))))))))
(dolist (case '(("A\n\nB" ("A" "" "B"))
("\nA" ("" "A"))
("A\n" ("A" ""))
("\n" ("" ""))))
(should (equal (mapcar #'string-trim-right
(normal-lines (car case)))
(cadr case))))
(should (equal (normal-lines " A") '(" A")))
(should (equal (normal-lines " ") '(" ")))
(should
(equal (mapcar #'string-trim-right
(normal-lines
"abcdef x" (* 5 (ebox--string-pixel-width "a"))))
'("abcde" "f x")))
(should
(= (length
(normal-lines
(make-string 10 ?\s)
(* 5 (ebox--string-pixel-width " "))))
2))
(should (= (length (normal-lines "word" 0)) 1))
(should (= (length (normal-lines " " 0)) 1))
(let* ((inline-zero
(ebox-box-create
:layout (ebox-normal-layout-create) :outer 'inline
:width '(10)
:children (list (ebox-text-create :value "I"))))
(zero-parent
(ebox-box-create
:layout (ebox-normal-layout-create) :width '(0)
:overflow 'visible :children (list inline-zero))))
(should (= (ebox-string-height (ebox-render zero-parent)) 1))))))
(ert-deftest ebox-canonical-row-box-retains-and-renders-typed-children () (ert-deftest ebox-canonical-row-box-retains-and-renders-typed-children ()
"A Row Box should retain one Box identity around all typed children." "A Row Box should retain one Box identity around all typed children."
(let* ((left (ebox-text-create :value "A" :source-handle 'left)) (let* ((left (ebox-text-create :value "A" :source-handle 'left))
(right (ebox-text-create :value "B" :source-handle 'right)) (right (ebox-text-create :value "B\nB\nB" :source-handle 'right))
(box (ebox-box-create :layout (ebox-row-layout-create) (box (ebox-box-create :layout (ebox-row-layout-create
:item-gap 5 :cross-align 'center)
:children (list left right) :children (list left right)
:source-handle 'row))) :source-handle 'row)))
(should (ebox-box-node-p box)) (should (ebox-box-node-p box))
(should (eq (ebox-node-source-handle box) 'row)) (should (eq (ebox-node-source-handle box) 'row))
(should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'row)) (should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'row))
(should (equal (ebox-layout-config-props (ebox-box-node-layout box))
'(:item-gap 5 :cross-align center)))
(should (equal (ebox-box-node-children box) (list left right))) (should (equal (ebox-box-node-children box) (list left right)))
(should (eq (ebox-tree-node-children box) (should (eq (ebox-tree-node-children box)
(ebox-box-node-children box))) (ebox-box-node-children box)))
@ -197,16 +269,26 @@
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3)) (should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3))
(should-not (plist-member box :ebox-content-node)) (should-not (plist-member box :ebox-content-node))
(should (equal (ebox--computed-display box) '(block row))) (should (equal (ebox--computed-display box) '(block row)))
(should (string= (ebox-dsl-test--plain box) "AB")))) (let* ((rendered (ebox-render box))
(lines (ebox-string-lines rendered))
(middle (nth 1 lines)))
(should-not (string-match-p "A" (substring-no-properties (car lines))))
(should (string-match-p "A.*B" (substring-no-properties middle)))
(should (= (ebox--string-pixel-width middle)
(+ (ebox--string-pixel-width "A") 5
(ebox--string-pixel-width "B")))))))
(ert-deftest ebox-canonical-column-box-retains-and-renders-typed-children () (ert-deftest ebox-canonical-column-box-retains-and-renders-typed-children ()
"A Column Box should retain one Box identity around all typed children." "A Column Box should retain one Box identity around all typed children."
(let* ((top (ebox-text-create :value "A")) (let* ((top (ebox-text-create :value "A"))
(bottom (ebox-text-create :value "B")) (bottom (ebox-text-create :value "BBB"))
(box (ebox-box-create :layout (ebox-column-layout-create) (box (ebox-box-create :layout (ebox-column-layout-create
:item-gap 1 :cross-align 'end)
:children (list top bottom)))) :children (list top bottom))))
(should (ebox-box-node-p box)) (should (ebox-box-node-p box))
(should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'column)) (should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'column))
(should (equal (ebox-layout-config-props (ebox-box-node-layout box))
'(:item-gap 1 :cross-align end)))
(should (equal (ebox-box-node-children box) (list top bottom))) (should (equal (ebox-box-node-children box) (list top bottom)))
(should (eq (ebox-tree-node-children box) (should (eq (ebox-tree-node-children box)
(ebox-box-node-children box))) (ebox-box-node-children box)))
@ -214,7 +296,21 @@
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3)) (should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3))
(should-not (plist-member box :ebox-content-node)) (should-not (plist-member box :ebox-content-node))
(should (equal (ebox--computed-display box) '(block column))) (should (equal (ebox--computed-display box) '(block column)))
(should (string= (ebox-dsl-test--plain box) "A\nB")))) (should (= (ebox-string-height (ebox-render box)) 3))
(let ((lines (ebox-string-lines (ebox-render box))))
(should (string-prefix-p " " (substring-no-properties (car lines))))
(should (= (ebox--string-pixel-width (car lines))
(ebox--string-pixel-width (nth 2 lines)))))))
(ert-deftest ebox-canonical-simple-axis-config-validates-closed-schema ()
"Row/Column configs should distinguish defaults from invalid explicit values."
(dolist (constructor '(ebox-row-layout-create ebox-column-layout-create))
(should (equal (ebox-layout-config-props (funcall constructor))
'(:item-gap 0 :cross-align stretch)))
(dolist (plist '((:item-gap nil) (:item-gap -1)
(:cross-align nil) (:cross-align bogus)
(:gap 1) (:item-gap 1 :item-gap 2)))
(should-error (apply constructor plist) :type 'error))))
(ert-deftest ebox-canonical-flex-box-uses-one-box-identity-and-child-list () (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." "A FlexConfig should select the Box algorithm without a Flex runtime node."