refactor: unify canonical style and font projection

This commit is contained in:
Kinneyzhang 2026-08-28 22:06:30 +08:00
parent cb0e1823d3
commit 722791a01e
38 changed files with 2491 additions and 1171 deletions

View File

@ -30,7 +30,7 @@ Use the functions and properties in `docs/user/ebox-api-reference.en.md`; the fa
## Layout scope ## Layout scope
Ebox supports character and pixel dimensions, padding, margins, borders, colors, Emacs face/font properties, overflow, wrapping, row/column/flex formatting, and Grid tracks including fixed, fractional, implicit, gap, placement, span, and alignment behavior. The supported typography subset is `:font`, `:font-family`, `:font-height`/`:font-size`, `:font-weight`, and `:font-slant`. CSS compatibility is intentionally partial: percentages, absolute positioning, z-index, shadows, border radius, full browser typography, and browser-level bidi are outside this package. Ebox supports column- and pixel-based horizontal dimensions, line-based vertical dimensions, padding, margins, pixel borders, colors, overflow, wrapping, row/column/flex formatting, and Grid tracks including fixed, fractional, implicit, gap, placement, span, and alignment behavior. The public typography subset is `:font-family`, CSS-reference-pixel `:font-size`, `:font-weight`, and `:font-style`; `:font-slant` is only an exact parse-time alias of `:font-style`. Emacs faces and text properties belong to the final adapter and are not Ebox author properties. CSS compatibility is intentionally partial: percentages, absolute positioning, z-index, shadows, border radius, full browser typography, and browser-level bidi are outside this package.
## Verification rule ## Verification rule

View File

@ -30,7 +30,7 @@ Ebox 是底层空间渲染引擎。它负责把声明式节点树转换为经过
## 布局范围 ## 布局范围
Ebox 支持字符和像素尺寸、padding、margin、border、颜色、Emacs face/font property、overflow、换行、row/column/flex formatting以及固定、分数、隐式、gap、放置、span 和对齐等 Grid 能力。支持的 typography 子集是 `:font`、`:font-family`、`:font-height`/`:font-size`、`:font-weight` 和 `:font-slant`。CSS 兼容性有意是部分实现百分比、绝对定位、z-index、阴影、border radius、完整浏览器 typography 和浏览器级 bidi 不属于本包。 Ebox 支持以列或像素表示的横向尺寸、以行表示的纵向尺寸、padding、margin、像素 border、颜色、overflow、换行、row/column/flex formatting以及固定、分数、隐式、gap、放置、span 和对齐等 Grid 能力。公共 typography 子集是 `:font-family`、以 CSS reference pixel 表示的 `:font-size`、`:font-weight` 和 `:font-style``:font-slant` 只是 `:font-style` 的精确 parse-time alias。Emacs face 与文本属性只属于最终 adapter不是 Ebox 作者属性。CSS 兼容性有意是部分实现百分比、绝对定位、z-index、阴影、border radius、完整浏览器 typography 和浏览器级 bidi 不属于本包。
## 验证规则 ## 验证规则

View File

@ -10,7 +10,7 @@ NATIVE_MANIFEST = native/Cargo.toml
NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))') NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))')
NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release
.PHONY: all check ci load compile test checkdoc source-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install .PHONY: all check ci load compile test checkdoc source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install
all: check all: check
@ -25,11 +25,14 @@ compile:
rm -f *.elc tests/*.elc scripts/*.elc rm -f *.elc tests/*.elc scripts/*.elc
$(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t byte-compile-warnings (quote (not obsolete)))' -l ebox.el --eval '(ebox-byte-compile)' $(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t byte-compile-warnings (quote (not obsolete)))' -l ebox.el --eval '(ebox-byte-compile)'
test: source-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests test: source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests
source-tests: source-tests:
$(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit $(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit
font-tests:
$(EMACS_BATCH) -l tests/ebox-font-tests.el -f ert-run-tests-batch-and-exit
core-tests: core-tests:
$(EMACS_TEST) -l tests/ebox-core-render-tests.el -f ert-run-tests-batch-and-exit $(EMACS_TEST) -l tests/ebox-core-render-tests.el -f ert-run-tests-batch-and-exit

View File

@ -33,7 +33,7 @@ field-based child syntax.
"*Ebox Example*" "*Ebox Example*"
(ebox-build (ebox-build
'(column :padding (1 2) '(column :padding (1 2)
:border ((1) solid "#8A93A6") :border (1 solid "#8A93A6")
(text :color "#263244" "Hello Ebox") (text :color "#263244" "Hello Ebox")
(row :item-gap 1 (row :item-gap 1
(box "Left") (box "Left")

View File

@ -30,7 +30,7 @@ Ebox 需要 Emacs 29.1 或更高版本,并依赖 ECSS 与 TP。包管理器应
"*Ebox Example*" "*Ebox Example*"
(ebox-build (ebox-build
'(column :padding (1 2) '(column :padding (1 2)
:border ((1) solid "#8A93A6") :border (1 solid "#8A93A6")
(text :color "#263244" "Hello Ebox") (text :color "#263244" "Hello Ebox")
(row :item-gap 1 (row :item-gap 1
(box "Left") (box "Left")

View File

@ -90,7 +90,7 @@ Caller-owned Source Tree
## Grid contract ## Grid contract
Grid currently covers fixed and pixel tracks, `auto`, fractional tracks, `minmax`, `repeat`, implicit rows and columns, row/column gap, auto-flow, one-based placement, positive spans, item/content alignment, and ordinary buffer rendering and updates. Ebox supports a finite typography subset (`:font`, `:font-family`, `:font-height`/`:font-size`, `:font-weight`, and `:font-slant`); full browser typography is outside the contract. Percentages, absolute positioning, z-index, border radius, shadows, and browser-level bidi are also outside the contract. Grid currently covers fixed and pixel tracks, `auto`, fractional tracks, `minmax`, `repeat`, implicit rows and columns, row/column gap, auto-flow, one-based placement, positive spans, item/content alignment, and ordinary buffer rendering and updates. Ebox supports canonical font inputs (`:font-family`, `:font-size`, `:font-weight`, and `:font-style`) plus text-decoration paint; full browser typography is outside the contract. Percentages, absolute positioning, z-index, border radius, shadows, and browser-level bidi are also outside the contract.
## Active examples and tests ## Active examples and tests

View File

@ -90,7 +90,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
## Grid 合同 ## Grid 合同
当前 Grid 支持固定和像素轨道、`auto`、分数轨道、`minmax`、`repeat`、隐式行列、行列 gap、auto-flow、从 1 开始的 placement、正整数 span、item/content 对齐,以及普通 buffer 渲染和更新。Ebox 支持有限 typography 子集(`:font`、`:font-family`、`:font-height`/`:font-size`、`:font-weight` 和 `:font-slant`;完整浏览器 typography 不在合同内。百分比、绝对定位、z-index、圆角、阴影和浏览器级 bidi 也不在合同内。 当前 Grid 支持固定和像素轨道、`auto`、分数轨道、`minmax`、`repeat`、隐式行列、行列 gap、auto-flow、从 1 开始的 placement、正整数 span、item/content 对齐,以及普通 buffer 渲染和更新。Ebox 支持规范字体输入(`:font-family`、`:font-size`、`:font-weight`、`:font-style`)与 text-decoration paint;完整浏览器 typography 不在合同内。百分比、绝对定位、z-index、圆角、阴影和浏览器级 bidi 也不在合同内。
## Active 示例与测试 ## Active 示例与测试

View File

@ -72,7 +72,9 @@ another public node or wrapper.
| Overflow | `:overflow` (`visible`, `hidden`, `scroll`) | | Overflow | `:overflow` (`visible`, `hidden`, `scroll`) |
Ordinary horizontal numbers are character columns; a one-element horizontal Ordinary horizontal numbers are character columns; a one-element horizontal
list is pixels. Vertical numbers are lines. `(viewport)` and list is pixels. Vertical numbers are lines. Border widths are the exception:
they are always nonnegative integer pixels, so use `(1 solid "#687386")`, not
`((1) solid "#687386")`. `(viewport)` and
`(viewport-height)` use the current render context. Property validation belongs `(viewport-height)` use the current render context. Property validation belongs
to Ebox and invalid context/value combinations signal an error. to Ebox and invalid context/value combinations signal an error.

View File

@ -67,8 +67,9 @@ participation property 就是普通子 Box property不会创建另一种公
| 文本布局 | `:text-align`、`:wrap-mode``word`、`char`、`kp`、`none` | | 文本布局 | `:text-align`、`:wrap-mode``word`、`char`、`kp`、`none` |
| Overflow | `:overflow``visible`、`hidden`、`scroll` | | Overflow | `:overflow``visible`、`hidden`、`scroll` |
普通横向数字表示字符列,单元素横向 list 表示像素;纵向数字表示行数。 普通横向数字表示字符列,单元素横向 list 表示像素纵向数字表示行数。Border
`(viewport)``(viewport-height)` 使用当前 render context。property 验证属于 width 是例外,始终使用非负整数像素,因此写 `(1 solid "#687386")`,不写
`((1) solid "#687386")`。`(viewport)` 与 `(viewport-height)` 使用当前 render context。property 验证属于
Eboxcontext/value 组合无效时会直接报错。 Eboxcontext/value 组合无效时会直接报错。
## 2. Evaluated typed construction ## 2. Evaluated typed construction

View File

@ -38,7 +38,7 @@ child-layout algorithm, not a different kind of visual object.
(defvar ebox-guide-input (defvar ebox-guide-input
(ebox-build (ebox-build
'(column :padding (1 2) '(column :padding (1 2)
:border ((1) solid "#8A93A6") :border (1 solid "#8A93A6")
(text :color "#263244" "Research notes") (text :color "#263244" "Research notes")
(row :item-gap 1 (row :item-gap 1
(box :id "status" :background-color "#F4F6FB" "Inbox") (box :id "status" :background-color "#F4F6FB" "Inbox")
@ -94,7 +94,7 @@ child `box`.
```elisp ```elisp
(ebox-build (ebox-build
'(grid :width (640) '(grid :width (640)
:grid-template-columns ((200) 1fr 1fr) :grid-template-columns ((200) (fr 1) (fr 1))
:grid-template-rows (1 1) :grid-template-rows (1 1)
:gap (1 (12)) :gap (1 (12))
(box :grid-column (1 :span 3) "Header") (box :grid-column (1 :span 3) "Header")
@ -117,7 +117,7 @@ Text Box geometry.
'(box :width (420) '(box :width (420)
:padding (1 2) :padding (1 2)
:margin (0 1) :margin (0 1)
:border ((1) solid "#8A93A6") :border (1 solid "#8A93A6")
:color "#263244" :color "#263244"
:background-color "#FFFFFF" :background-color "#FFFFFF"
"A readable panel")) "A readable panel"))

View File

@ -34,7 +34,7 @@ Ebox 文档由 Text 和 Box 节点组成。author 语法只有七个入口:
(defvar ebox-guide-input (defvar ebox-guide-input
(ebox-build (ebox-build
'(column :padding (1 2) '(column :padding (1 2)
:border ((1) solid "#8A93A6") :border (1 solid "#8A93A6")
(text :color "#263244" "Research notes") (text :color "#263244" "Research notes")
(row :item-gap 1 (row :item-gap 1
(box :id "status" :background-color "#F4F6FB" "Inbox") (box :id "status" :background-color "#F4F6FB" "Inbox")
@ -86,7 +86,7 @@ placement property 同样直接属于子 `box`。
```elisp ```elisp
(ebox-build (ebox-build
'(grid :width (640) '(grid :width (640)
:grid-template-columns ((200) 1fr 1fr) :grid-template-columns ((200) (fr 1) (fr 1))
:grid-template-rows (1 1) :grid-template-rows (1 1)
:gap (1 (12)) :gap (1 (12))
(box :grid-column (1 :span 3) "Header") (box :grid-column (1 :span 3) "Header")
@ -107,7 +107,7 @@ placement property 同样直接属于子 `box`。
'(box :width (420) '(box :width (420)
:padding (1 2) :padding (1 2)
:margin (0 1) :margin (0 1)
:border ((1) solid "#8A93A6") :border (1 solid "#8A93A6")
:color "#263244" :color "#263244"
:background-color "#FFFFFF" :background-color "#FFFFFF"
"A readable panel")) "A readable panel"))

View File

@ -6,9 +6,24 @@
;;; Code: ;;; Code:
(define-error 'ebox-surface-capability-error
"Ebox surface capability violation")
(defun ebox-buffer-validate-border-capability (style)
"Return STYLE when its border can be represented exactly by Emacs text."
(dolist (side '(top bottom) style)
(let ((width (or (plist-get
style (intern (format ":border-%s-pixel" side)))
0)))
(when (> width 1)
(signal 'ebox-surface-capability-error
(list :border side :declared-pixels width
:supported-pixels '(0 1)))))))
(require 'cl-lib) (require 'cl-lib)
(require 'subr-x) (require 'subr-x)
(require 'ebox-measure) (require 'ebox-measure)
(require 'ebox-font)
(require 'tp-style) (require 'tp-style)
(defvar ebox--propertize-private-content-line-p nil (defvar ebox--propertize-private-content-line-p nil
@ -320,29 +335,50 @@ Return FUNCTION's value."
;;; Paint Mapping ;;; Paint Mapping
(defun ebox-buffer--font-attributes (style) (defun ebox-buffer--font-fact (style)
"Return anonymous face attributes for STYLE typography longhands." "Return STYLE's shared surface-resolved font fact."
(let (face) (and (listp style) (plist-get style :ebox-font-fact)))
(dolist (entry '((:font-family . :family)
(:font-height . :height)
(:font-weight . :weight)
(:font-slant . :slant)))
(when-let* ((value (plist-get style (car entry))))
(setq face (plist-put face (cdr entry) value))))
face))
(defun ebox-buffer--font-face (style) (defun ebox-buffer--font-face (style)
"Return STYLE's complete Emacs face value for typography." "Return a detached Emacs face from STYLE's shared font fact."
(let* ((base (plist-get style :font)) (when-let* ((fact (ebox-buffer--font-fact style)))
(base (if (stringp base) (list :family base) base)) (ebox-font-paint-face fact)))
(attributes (ebox-buffer--font-attributes style)))
(cond (defconst ebox-buffer--decoration-style-map
((and attributes base) '((solid . line) (double . double-line) (dotted . dots)
(if (and (listp base) (not (keywordp (car-safe base)))) (dashed . dashes) (wavy . wave))
(cons attributes base) "Canonical CSS decoration styles mapped to Emacs underline styles.")
(list attributes base)))
(attributes attributes) (defun ebox-buffer--text-decoration-face (style)
(base base)))) "Return STYLE's final Emacs text-decoration face, or nil."
(let* ((line (plist-get style :text-decoration-line))
(lines (if (listp line) line (list line)))
(color (plist-get style :text-decoration-color))
(decoration-style
(or (plist-get style :text-decoration-style) 'solid))
(resolved-color
(unless (or (memq color '(nil currentColor))
(tp-paint-slot-p color))
color))
face)
(unless (or (null line) (equal lines '(none)))
(when (memq 'underline lines)
(setq face
(plist-put
face :underline
(append
(when resolved-color (list :color resolved-color))
(let ((emacs-style
(alist-get decoration-style
ebox-buffer--decoration-style-map)))
(unless (eq emacs-style 'line)
(list :style emacs-style)))))))
(when (memq 'overline lines)
(setq face (plist-put face :overline (or resolved-color t))))
(when (memq 'line-through lines)
(setq face (plist-put face :strike-through
(or resolved-color t)))))
face))
(defun ebox-buffer--paint-color (style role) (defun ebox-buffer--paint-color (style role)
"Return the foreground paint color for STYLE and ROLE." "Return the foreground paint color for STYLE and ROLE."
@ -377,6 +413,8 @@ foreground color."
This backend mapper intentionally accepts computed style facts and emits only This backend mapper intentionally accepts computed style facts and emits only
buffer-facing paint properties. Layout-only properties never pass through." buffer-facing paint properties. Layout-only properties never pass through."
(let ((face (ebox-buffer--font-face style))) (let ((face (ebox-buffer--font-face style)))
(when-let* ((decoration (ebox-buffer--text-decoration-face style)))
(setq face (append face decoration)))
(when-let* ((foreground (ebox-buffer--paint-color style role))) (when-let* ((foreground (ebox-buffer--paint-color style role)))
(setq face (setq face
(cond (cond
@ -397,7 +435,17 @@ buffer-facing paint properties. Layout-only properties never pass through."
(defun ebox--propertize-typography (string style) (defun ebox--propertize-typography (string style)
"Apply STYLE's typography to one copy of STRING." "Apply STYLE's typography to one copy of STRING."
(if-let* ((face (ebox-buffer--font-face style))) (if-let* ((fact (ebox-buffer--font-fact style)))
(let ((copy (copy-sequence string))
(face (ebox-font-measurement-face fact)))
(when face
(ebox--add-render-face! copy 0 (length copy) face t))
(ebox--register-render-owned-face-values string copy))
string))
(defun ebox--propertize-text-decoration (string style)
"Apply STYLE's text decoration to one copy of STRING."
(if-let* ((face (ebox-buffer--text-decoration-face style)))
(let ((copy (copy-sequence string))) (let ((copy (copy-sequence string)))
(ebox--add-render-face! copy 0 (length copy) face t) (ebox--add-render-face! copy 0 (length copy) face t)
(ebox--register-render-owned-face-values string copy)) (ebox--register-render-owned-face-values string copy))

View File

@ -242,9 +242,11 @@ canonical inputs, not additional source authority."
(plist-put node :ebox-source-handle handle) (plist-put node :ebox-source-handle handle)
node) node)
(defun ebox-canonical--text-measurement-property-p (property) (defun ebox-canonical--text-engine-property-p (_property)
"Return non-nil when PROPERTY belongs to canonical Text measurement." "Return non-nil for a Text fact materializable without a Surface.
(eq (plist-get property :group) 'typography)) Canonical Text style currently consists of paint facts and host-resolved font
facts. Neither is a constructor-owned measurement field."
nil)
(defun ebox-canonical--box-frame-property-p (property) (defun ebox-canonical--box-frame-property-p (property)
"Return non-nil when PROPERTY belongs to Box geometry or participation." "Return non-nil when PROPERTY belongs to Box geometry or participation."
@ -284,20 +286,18 @@ and author TAG."
declarations declarations
(lambda (property) (lambda (property)
(or (funcall predicate property) (or (funcall predicate property)
(memq (plist-get property :group) (eq (plist-get property :group) 'paint))))
'(paint typography)))))
(ebox-style-declarations-require-projection-p (ebox-style-declarations-require-projection-p
declarations declarations
(lambda (property) (lambda (property)
(or (funcall predicate property) (or (funcall predicate property)
(memq (plist-get property :group) (eq (plist-get property :group) 'paint)
'(paint typography))
(and (memq tag '(flex grid)) (and (memq tag '(flex grid))
(memq tag (memq tag
(plist-get property (plist-get property
:contexts))))))))) :contexts)))))))))
(setf (nth 1 facts) (setf (nth 1 facts)
(ebox-style-expand-ebox-plist (nth 1 facts))) (ebox-style--expand-engine-plist (nth 1 facts)))
(puthash tag facts by-tag) (puthash tag facts by-tag)
facts)))) facts))))
(let ((declarations (let ((declarations
@ -316,13 +316,12 @@ and author TAG."
do (error "%s does not accept %S" do (error "%s does not accept %S"
context (plist-get property :name))) context (plist-get property :name)))
(list declarations (list declarations
(ebox-style-expand-ebox-plist (ebox-style--expand-engine-plist
(ebox-style-declaration-properties (ebox-style-declaration-properties
declarations declarations
(lambda (property) (lambda (property)
(or (funcall predicate property) (or (funcall predicate property)
(memq (plist-get property :group) (eq (plist-get property :group) 'paint)))))
'(paint typography))))))
nil)))) nil))))
(defun ebox-canonical-facts-from-declarations (tag declarations) (defun ebox-canonical-facts-from-declarations (tag declarations)
@ -331,7 +330,7 @@ Source declarations stay behind the source handle; the returned value contains
only the current canonical-node projection needed before G4c separates paint." only the current canonical-node projection needed before G4c separates paint."
(let ((predicate (let ((predicate
(if (eq tag 'text) (if (eq tag 'text)
#'ebox-canonical--text-measurement-property-p #'ebox-canonical--text-engine-property-p
#'ebox-canonical--box-frame-property-p))) #'ebox-canonical--box-frame-property-p)))
(pcase-let ((`(,canonical ,engine ,style-required-p) (pcase-let ((`(,canonical ,engine ,style-required-p)
(ebox-canonical--node-properties (ebox-canonical--node-properties

View File

@ -73,31 +73,49 @@
(when-let* ((entry (ebox-dsl--optional-field properties field context))) (when-let* ((entry (ebox-dsl--optional-field properties field context)))
(cdr entry))) (cdr entry)))
(defun ebox-dsl--validate-source-fields (properties context)
"Validate shared source metadata in PROPERTIES for CONTEXT."
(dolist (entry `((:key . ,#'ebox-source-stable-key-p)
(:class . ,#'ebox-source-class-list-p)
(:id . ,#'ebox-source-semantic-id-p)))
(when-let* ((field (car entry))
(present (ebox-dsl--optional-field properties field context)))
(unless (funcall (cdr entry) (cdr present))
(error "%s rejected %S value: %S" context field (cdr present)))))
properties)
(defun ebox-dsl--compile-style (tag properties context) (defun ebox-dsl--compile-style (tag properties context)
"Compile and validate TAG author PROPERTIES for CONTEXT." "Compile and validate TAG author PROPERTIES for CONTEXT."
(condition-case err (condition-case err
(if properties (let ((declarations
(ebox-style-compile-form tag properties) (and properties (ebox-style-compile-form tag properties))))
nil) (cl-loop
for (property value)
on (ebox-style-declaration-properties
declarations
(lambda (definition)
(eq (plist-get definition :normalizer) 'color)))
by #'cddr
unless (stringp value)
do (error "Standalone Ebox %S requires a CSS color string: %S"
property value))
declarations)
(error (error
(error "%s rejected properties: %s" (error "%s rejected properties: %s"
context (error-message-string err))))) context (error-message-string err)))))
(defun ebox-dsl--layout-config (defun ebox-dsl--layout-config (tag declarations context)
(tag declarations extensions context) "Construct TAG LayoutConfig from canonical DECLARATIONS for CONTEXT."
"Construct TAG LayoutConfig from DECLARATIONS and EXTENSIONS for CONTEXT."
(if (eq tag 'text) (if (eq tag 'text)
nil nil
(condition-case err (condition-case err
(ebox-layout-config-for-form (let ((names (ebox-layout-config-property-names tag)))
tag (ebox-layout-config-for-form
(if (memq tag '(row column)) tag
extensions (ebox-style-declaration-properties
(let ((names (ebox-layout-config-property-names tag))) declarations
(ebox-style-declaration-properties (lambda (property)
declarations (memq (plist-get property :name) names)))))
(lambda (property)
(memq (plist-get property :name) names))))))
(error (error
(error "%s rejected layout config: %s" (error "%s rejected layout config: %s"
context (error-message-string err)))))) context (error-message-string err))))))
@ -114,6 +132,7 @@ function."
(let* ((context (format "Ebox %S" tag)) (let* ((context (format "Ebox %S" tag))
(properties (properties
(ebox-dsl--validate-plist properties context))) (ebox-dsl--validate-plist properties context)))
(ebox-dsl--validate-source-fields properties context)
(dolist (field '(:content :layout :display :ebox-type :ebox-content-node)) (dolist (field '(:content :layout :display :ebox-type :ebox-content-node))
(when (plist-member properties field) (when (plist-member properties field)
(error "%s does not accept private author property %S" (error "%s does not accept private author property %S"
@ -124,17 +143,9 @@ function."
properties :outer context))) properties :outer context)))
(outer (and (not (eq tag 'text)) (outer (and (not (eq tag 'text))
(if outer-field (cdr outer-field) 'block))) (if outer-field (cdr outer-field) 'block)))
(extension-keys (and (memq tag '(row column))
'(:item-gap :cross-align)))
(extensions
(ebox-dsl--keep-keys
properties extension-keys))
(style-properties (style-properties
(ebox-dsl--without-keys (ebox-dsl--without-keys
properties properties ebox-dsl--source-fields))
(append (unless (eq tag 'text) '(:outer))
extension-keys
ebox-dsl--source-fields)))
(declarations (declarations
(ebox-dsl--compile-style (ebox-dsl--compile-style
tag style-properties context))) tag style-properties context)))
@ -152,7 +163,7 @@ function."
:outer outer :outer outer
:layout :layout
(ebox-dsl--layout-config (ebox-dsl--layout-config
tag declarations extensions context) tag declarations context)
:declarations declarations :declarations declarations
:owned-facts :owned-facts
(ebox-canonical-facts-from-declarations tag declarations))))) (ebox-canonical-facts-from-declarations tag declarations)))))

View File

@ -102,18 +102,17 @@
"Supported canonical FlexConfig wrapping values.") "Supported canonical FlexConfig wrapping values.")
(defconst ebox--flex-justify-content-values (defconst ebox--flex-justify-content-values
'(flex-start flex-end center space-between space-around space-evenly '(normal start end flex-start flex-end center stretch
start end left right normal) space-between space-around space-evenly)
"Supported canonical FlexConfig main-axis alignment values.") "Supported canonical FlexConfig main-axis alignment values.")
(defconst ebox--flex-align-items-values (defconst ebox--flex-align-items-values
'(stretch flex-start flex-end center baseline normal '(normal start end stretch flex-start flex-end center baseline)
start end self-start self-end)
"Supported canonical FlexConfig item alignment values.") "Supported canonical FlexConfig item alignment values.")
(defconst ebox--flex-align-content-values (defconst ebox--flex-align-content-values
'(stretch flex-start flex-end center space-between space-around space-evenly '(normal start end stretch flex-start flex-end center
baseline normal start end) space-between space-around space-evenly)
"Supported canonical FlexConfig line alignment values.") "Supported canonical FlexConfig line alignment values.")
(defconst ebox--flex-container-neutral-box-prop-keys (defconst ebox--flex-container-neutral-box-prop-keys
@ -1843,7 +1842,7 @@ a prefix of the eventual full wrapper render."
(unless complete (unless complete
(plist-put copy :padding-bottom-height 0) (plist-put copy :padding-bottom-height 0)
(plist-put copy :margin-bottom-height 0) (plist-put copy :margin-bottom-height 0)
(plist-put copy :border-bottom-p nil)) (plist-put copy :border-bottom-pixel 0))
copy)) copy))
(defun ebox--flex-window-complete-prefix-supported-p (box) (defun ebox--flex-window-complete-prefix-supported-p (box)
@ -1858,7 +1857,7 @@ a prefix of the eventual full wrapper render."
line line
(let ((region-id (ebox--ensure-region-id box))) (let ((region-id (ebox--ensure-region-id box)))
(puthash region-id box ebox--region-box-table) (puthash region-id box ebox--region-box-table)
(if (ebox-get box :border-bottom-p) (if (> (ebox-get box :border-bottom-pixel) 0)
(propertize (propertize
(ebox--propertize-underline (ebox--propertize-underline
line (ebox-get box :border-bottom-color)) line (ebox-get box :border-bottom-color))
@ -1918,7 +1917,7 @@ the chunk begins at the wrapper top and should include top chrome."
(border-right-pixel (ebox-get box :border-right-pixel)) (border-right-pixel (ebox-get box :border-right-pixel))
(border-right-color (ebox-get box :border-right-color)) (border-right-color (ebox-get box :border-right-color))
(border-top-p (and include-top (border-top-p (and include-top
(ebox-get box :border-top-p))) (> (ebox-get box :border-top-pixel) 0)))
(border-top-color (ebox-get box :border-top-color)) (border-top-color (ebox-get box :border-top-color))
(color (and (ebox-style--text-paint-owner-p box) (color (and (ebox-style--text-paint-owner-p box)
(ebox-get box :color))) (ebox-get box :color)))
@ -2242,7 +2241,7 @@ the chunk begins at the wrapper top and should include top chrome."
"Return how many cached prefix tail lines completion rewrites for NODE." "Return how many cached prefix tail lines completion rewrites for NODE."
(let ((box (ebox--flex-window-box node))) (let ((box (ebox--flex-window-box node)))
(if (and box (if (and box
(ebox-get box :border-bottom-p) (> (ebox-get box :border-bottom-pixel) 0)
(= (floor (ebox-get box :padding-bottom-height)) 0)) (= (floor (ebox-get box :padding-bottom-height)) 0))
1 1
0))) 0)))

222
ebox-font.el Normal file
View File

@ -0,0 +1,222 @@
;;; ebox-font.el --- Surface-resolved font facts -*- lexical-binding: t; -*-
;;; Commentary:
;; Resolves canonical font inputs against one Emacs display capability and
;; returns a shared immutable fact for measurement and final paint. It does
;; not own author parsing, cascade, layout, TP priority, or buffer publication.
;;; Code:
(require 'cl-lib)
(cl-defstruct (ebox-font-fact
(:constructor ebox-font--make-fact)
(:conc-name ebox-font-fact--))
"One surface-resolved font projection shared by all downstream consumers."
(requested-decipoints nil :read-only t)
(resolved-font-identity nil :read-only t)
(measurement-metrics nil :read-only t)
(capability-signature nil :read-only t))
(defun ebox-font-fact-requested-decipoints (fact)
"Return FACT's requested decipoints scalar."
(ebox-font-fact--requested-decipoints fact))
(defun ebox-font-fact-resolved-font-identity (fact)
"Return a detached resolved identity from FACT."
(copy-tree (ebox-font-fact--resolved-font-identity fact) t))
(defun ebox-font-fact-measurement-metrics (fact)
"Return detached measurement metrics from FACT."
(copy-tree (ebox-font-fact--measurement-metrics fact) t))
(defun ebox-font-fact-capability-signature (fact)
"Return a detached display capability signature from FACT."
(copy-tree (ebox-font-fact--capability-signature fact) t))
(defconst ebox-font--cache-max-entries 512
"Maximum resolved font facts retained across display capabilities.")
(defvar ebox-font--cache (make-hash-table :test #'equal)
"Cache keyed by canonical font inputs and complete capability signature.")
(defun ebox-font--requested-decipoints (font-size-px)
"Convert positive FONT-SIZE-PX reference pixels to integer decipoints."
(when font-size-px
(unless (and (numberp font-size-px) (> font-size-px 0))
(error "Ebox font size must be positive CSS reference pixels: %S"
font-size-px))
;; CSS reference px is 1/96 inch. Emacs integer face height is 1/10 point.
;; px * 72 / 96 * 10 = px * 7.5; positive values round half up.
(floor (+ (* font-size-px 7.5) 0.5))))
(defun ebox-font--face
(family decipoints weight style weight-active-p style-active-p)
"Return Emacs face projection for canonical font inputs."
(let (face)
(when family
(setq face
(plist-put face :family
(if (symbolp family) (symbol-name family) family))))
(when decipoints (setq face (plist-put face :height decipoints)))
(when weight-active-p (setq face (plist-put face :weight weight)))
(when style-active-p (setq face (plist-put face :slant style)))
face))
(defun ebox-font--parent-css-weight (fact)
"Return FACT's resolved CSS numeric weight, defaulting to normal."
(or (and (ebox-font-fact-p fact)
(plist-get (ebox-font-fact--resolved-font-identity fact)
:css-weight))
400))
(defun ebox-font--absolute-css-weight (weight parent-fact)
"Resolve CSS WEIGHT against PARENT-FACT to one absolute numeric weight."
(pcase weight
((or 'nil 'normal) 400)
('bold 700)
('bolder
(let ((parent (ebox-font--parent-css-weight parent-fact)))
(cond ((<= parent 300) 400)
((<= parent 500) 700)
(t 900))))
('lighter
(let ((parent (ebox-font--parent-css-weight parent-fact)))
(cond ((<= parent 500) 100)
((<= parent 700) 400)
(t 700))))
((pred numberp) weight)
(_ (error "Invalid canonical CSS font weight: %S" weight))))
(defun ebox-font--emacs-weight (css-weight)
"Map absolute numeric CSS-WEIGHT to an Emacs face weight."
(alist-get css-weight
'((100 . ultra-light) (200 . extra-light) (300 . light)
(400 . normal) (500 . normal) (600 . semi-bold)
(700 . bold) (800 . extra-bold) (900 . ultra-bold))))
(defun ebox-font--default-resolver (face window)
"Resolve FACE in WINDOW and return identity plus actual measurement metrics."
(let* ((window (and (window-live-p window) window))
(probe (propertize "M " 'face face))
(font (ignore-errors (font-at 0 window probe)))
(name
(and font
(or (ignore-errors (font-get font :name))
(and (fboundp 'font-xlfd-name)
(ignore-errors (font-xlfd-name font))))))
(info
(and name
(ignore-errors
(font-info name (and window (window-frame window)))))))
(list
:identity
(if font
(list :type (font-get font :type)
:name name
:family (font-get font :family)
:weight (font-get font :weight)
:slant (font-get font :slant)
:width (font-get font :width)
:size (font-get font :size)
:file (and info (> (length info) 12) (aref info 12)))
(list :unresolved-face (copy-tree face)))
:metrics
(if info
(list :pixel-size (aref info 2)
:height (aref info 3)
:baseline-offset (aref info 4)
:max-width (aref info 7)
:ascent (aref info 8)
:descent (aref info 9)
:space-width (aref info 10)
:average-width (aref info 11))
(list :probe-width (string-pixel-width probe)
:space-width
(string-pixel-width (propertize " " 'face face)))))))
(defvar ebox-font--resolver #'ebox-font--default-resolver
"Function resolving an Emacs face into identity and measurement metrics.")
(cl-defun ebox-font--project
(&key family size weight style weight-active-p style-active-p
capability-signature window parent-fact)
"Return one cached font fact for canonical inputs and display capability."
(let* ((css-weight (ebox-font--absolute-css-weight weight parent-fact))
(emacs-weight (ebox-font--emacs-weight css-weight))
(decipoints (ebox-font--requested-decipoints size))
(face (ebox-font--face
family decipoints emacs-weight style
weight-active-p style-active-p))
(key (list family size weight css-weight style
weight-active-p style-active-p capability-signature))
(missing (make-symbol "ebox-font-fact-missing"))
(cached (gethash key ebox-font--cache missing)))
(if (not (eq cached missing))
cached
(let* ((resolved (funcall ebox-font--resolver face window))
(fact
(ebox-font--make-fact
:requested-decipoints decipoints
:resolved-font-identity
(list :css-weight css-weight
:face (copy-tree face)
:resource (copy-tree (plist-get resolved :identity)))
:measurement-metrics
(copy-tree (plist-get resolved :metrics))
:capability-signature (copy-tree capability-signature))))
(when (>= (hash-table-count ebox-font--cache)
ebox-font--cache-max-entries)
(clrhash ebox-font--cache))
(puthash (copy-tree key) fact ebox-font--cache)
fact))))
(defun ebox-font-project-values
(values specified capability-signature &optional window parent-fact)
"Project canonical font VALUES for one Emacs display capability.
SPECIFIED is the canonical property-id set with declaration winners."
(let* ((values values)
(family (plist-get values 'ebox/font-family))
(size (plist-get values 'ebox/font-size))
(weight (plist-get values 'ebox/font-weight))
(font-style (plist-get values 'ebox/font-style))
(weight-active-p
(or (not (eq weight 'normal))
(memq 'ebox/font-weight specified)))
(style-active-p
(or (not (eq font-style 'normal))
(memq 'ebox/font-style specified))))
(when (or family size weight-active-p style-active-p)
(ebox-font--project
:family family :size size :weight weight :style font-style
:weight-active-p weight-active-p :style-active-p style-active-p
:capability-signature capability-signature :window window
:parent-fact parent-fact))))
(defun ebox-font-clear-cache ()
"Clear all surface-resolved font facts."
(clrhash ebox-font--cache))
(defun ebox-font-measurement-face (fact)
"Return a detached measurement face from font FACT."
(unless (ebox-font-fact-p fact)
(signal 'wrong-type-argument (list 'ebox-font-fact-p fact)))
(copy-tree
(plist-get (ebox-font-fact--resolved-font-identity fact) :face)))
(defun ebox-font-paint-face (fact)
"Return a detached final-paint face from the same font FACT."
(unless (ebox-font-fact-p fact)
(signal 'wrong-type-argument (list 'ebox-font-fact-p fact)))
(copy-tree
(plist-get (ebox-font-fact--resolved-font-identity fact) :face)))
(defun ebox-font-fact-neutral-p (fact)
"Return non-nil when FACT adds no non-default font capability."
(and (ebox-font-fact-p fact)
(null (ebox-font-fact--requested-decipoints fact))
(null (plist-get
(ebox-font-fact--resolved-font-identity fact) :face))))
(provide 'ebox-font)
;;; ebox-font.el ends here

View File

@ -125,14 +125,14 @@ Backend marker and buffer span data are intentionally excluded."
signature)) signature))
(defconst ebox--paint-style-signature-keys (defconst ebox--paint-style-signature-keys
'(:color :bgcolor '(:color :bgcolor :visibility
:border-top-p :border-bottom-p :text-decoration-line :text-decoration-color :text-decoration-style
:border-left-color :border-right-color :border-left-color :border-right-color
:border-top-color :border-bottom-color) :border-top-color :border-bottom-color)
"Box properties that affect paint without changing layout geometry.") "Box properties that affect paint without changing layout geometry.")
(defconst ebox--typography-style-signature-keys (defconst ebox--typography-style-signature-keys
'(:font :font-family :font-height :font-weight :font-slant) '(:ebox-font-fact)
"Typography properties whose resolved metrics may change geometry.") "Typography properties whose resolved metrics may change geometry.")
(defconst ebox--geometry-style-signature-keys (defconst ebox--geometry-style-signature-keys
@ -145,7 +145,10 @@ Backend marker and buffer span data are intentionally excluded."
:padding-top-height :padding-bottom-height :padding-top-height :padding-bottom-height
:margin-left-pixel :margin-right-pixel :margin-left-pixel :margin-right-pixel
:margin-top-height :margin-bottom-height :margin-top-height :margin-bottom-height
:border-left-pixel :border-right-pixel :border-left-pixel :border-left-style
:border-right-pixel :border-right-style
:border-top-pixel :border-top-style
:border-bottom-pixel :border-bottom-style
:text-align :vertical-align :text-align :vertical-align
:overflow :wrap-mode :overflow :wrap-mode
:flex-props :flex-participation)) :flex-props :flex-participation))

View File

@ -18,6 +18,7 @@
(declare-function ebox--ensure-node-id "ebox" (node)) (declare-function ebox--ensure-node-id "ebox" (node))
(declare-function ebox--ensure-region-id "ebox" (box)) (declare-function ebox--ensure-region-id "ebox" (box))
(declare-function ebox-box-node-p "ebox-canonical" (node))
(declare-function ebox-get "ebox" (box property)) (declare-function ebox-get "ebox" (box property))
(declare-function ebox-node-factory--create-from-properties (declare-function ebox-node-factory--create-from-properties
"ebox-node-factory" (&rest plist)) "ebox-node-factory" (&rest plist))
@ -26,6 +27,10 @@
(declare-function ebox-string-lines "ebox" (string)) (declare-function ebox-string-lines "ebox" (string))
(declare-function ebox-string-height "ebox" (string)) (declare-function ebox-string-height "ebox" (string))
(declare-function ebox-lines-join "ebox" (lines)) (declare-function ebox-lines-join "ebox" (lines))
(declare-function ebox--side-pixel "ebox-layout" (box &optional side))
(declare-function ebox--content-min-pixel "ebox-layout" (box))
(declare-function ebox--content-max-pixel "ebox-layout" (box))
(declare-function ebox--line-min-content-pixel "ebox-layout" (line))
(declare-function ebox--lines-align-vertical "ebox-layout" (string height align)) (declare-function ebox--lines-align-vertical "ebox-layout" (string height align))
(declare-function ebox--flex-spacing "ebox-flex" (mode leftover count base-gap)) (declare-function ebox--flex-spacing "ebox-flex" (mode leftover count base-gap))
(declare-function ebox--render-with-cache "ebox-incremental" (node &optional force cache-probe)) (declare-function ebox--render-with-cache "ebox-incremental" (node &optional force cache-probe))
@ -34,7 +39,7 @@
(defconst ebox--grid-layout-prop-keys (defconst ebox--grid-layout-prop-keys
'(:grid-template-columns :grid-template-rows '(:grid-template-columns :grid-template-rows
:grid-auto-columns :grid-auto-rows :grid-auto-flow :grid-auto-columns :grid-auto-rows :grid-auto-flow
:column-gap :row-gap :grid-column-gap :grid-row-gap :gap :column-gap :row-gap :gap
:justify-items :align-items :justify-content :align-content) :justify-items :align-items :justify-content :align-content)
"Properties owned by the grid formatting context.") "Properties owned by the grid formatting context.")
@ -46,20 +51,20 @@
"Canonical property order stored in a typed GridConfig.") "Canonical property order stored in a typed GridConfig.")
(defconst ebox--grid-justify-items-values (defconst ebox--grid-justify-items-values
'(stretch normal start end left right center) '(stretch start end center)
"Supported canonical GridConfig inline item alignment values.") "Supported canonical GridConfig inline item alignment values.")
(defconst ebox--grid-align-items-values (defconst ebox--grid-align-items-values
'(stretch normal start end top bottom center) '(normal start end stretch flex-start flex-end center baseline)
"Supported canonical GridConfig block item alignment values.") "Supported canonical GridConfig block item alignment values.")
(defconst ebox--grid-content-alignment-values (defconst ebox--grid-content-alignment-values
'(stretch normal start end left right center '(normal start end stretch flex-start flex-end center
flex-start flex-end space-between space-around space-evenly) space-between space-around space-evenly)
"Supported canonical GridConfig content alignment values.") "Supported canonical GridConfig content alignment values.")
(defconst ebox--grid-item-prop-keys (defconst ebox--grid-item-prop-keys
'(:grid-column :grid-row :grid-column-span :grid-row-span) '(:grid-column :grid-row :justify-self)
"Properties that describe a child's position in a grid.") "Properties that describe a child's position in a grid.")
(defun ebox-grid--split-attrs (items) (defun ebox-grid--split-attrs (items)
@ -99,7 +104,17 @@
(unless (and (null (cdddr track)) (unless (and (null (cdddr track))
(integerp count) (> count 0) value) (integerp count) (> count 0) value)
(error "ebox-grid: invalid repeat track %S" track)) (error "ebox-grid: invalid repeat track %S" track))
(make-list count value)) (let ((body
(cond
((or (ebox-grid--track-form-p value)
(numberp value) (symbolp value)
(and (consp value) (numberp (car value))
(null (cdr value))))
(list value))
((proper-list-p value) value)
(t (error "ebox-grid: invalid repeat body %S" value)))))
(cl-loop repeat count
append (ebox-grid--expand-repeat body))))
(list track))) (list track)))
tracks)) tracks))
@ -110,14 +125,8 @@
value)) value))
(cond (cond
((eq value 'auto) '(:kind auto :factor 0)) ((eq value 'auto) '(:kind auto :factor 0))
((and (symbolp value) ((memq value '(min-content max-content))
(string-match "\\`\\([0-9]+\\(?:\\.[0-9]+\\)?\\)fr\\'" (list :kind value))
(symbol-name value)))
(let ((factor
(string-to-number (match-string 1 (symbol-name value)))))
(unless (> factor 0)
(error "ebox-grid: fractional track must be positive: %S" value))
(list :kind 'fr :factor factor)))
((and (consp value) (eq (car value) 'fr)) ((and (consp value) (eq (car value) 'fr))
(unless (and (numberp (cadr value)) (> (cadr value) 0)) (unless (and (numberp (cadr value)) (> (cadr value) 0))
(error "ebox-grid: invalid fractional track %S" value)) (error "ebox-grid: invalid fractional track %S" value))
@ -129,9 +138,11 @@
(error "ebox-grid: invalid minmax track %S" value)) (error "ebox-grid: invalid minmax track %S" value))
(let ((minimum (ebox-grid--normalize-track (cadr value) axis)) (let ((minimum (ebox-grid--normalize-track (cadr value) axis))
(maximum (ebox-grid--normalize-track (caddr value) axis))) (maximum (ebox-grid--normalize-track (caddr value) axis)))
(unless (memq (plist-get minimum :kind) '(fixed auto)) (unless (memq (plist-get minimum :kind)
'(fixed auto min-content max-content))
(error "ebox-grid: minmax minimum must be fixed or auto: %S" value)) (error "ebox-grid: minmax minimum must be fixed or auto: %S" value))
(unless (memq (plist-get maximum :kind) '(fixed auto fr)) (unless (memq (plist-get maximum :kind)
'(fixed auto min-content max-content fr))
(error "ebox-grid: minmax maximum cannot be nested: %S" value)) (error "ebox-grid: minmax maximum cannot be nested: %S" value))
(when (and (eq (plist-get maximum :kind) 'fixed) (when (and (eq (plist-get maximum :kind) 'fixed)
(eq (plist-get minimum :kind) 'fixed) (eq (plist-get minimum :kind) 'fixed)
@ -164,14 +175,10 @@
(let* ((pair (ebox--flex-gap-pair (plist-get props :gap))) (let* ((pair (ebox--flex-gap-pair (plist-get props :gap)))
(row (cond (row (cond
((plist-member props :row-gap) (plist-get props :row-gap)) ((plist-member props :row-gap) (plist-get props :row-gap))
((plist-member props :grid-row-gap)
(plist-get props :grid-row-gap))
(t (car pair)))) (t (car pair))))
(column (cond (column (cond
((plist-member props :column-gap) ((plist-member props :column-gap)
(plist-get props :column-gap)) (plist-get props :column-gap))
((plist-member props :grid-column-gap)
(plist-get props :grid-column-gap))
(t (cdr pair))))) (t (cdr pair)))))
(cons (or (ebox--flex-line-value row 0) 0) (cons (or (ebox--flex-line-value row 0) 0)
(or (ebox--nonnegative-horizontal-size-pixels column 0) 0)))) (or (ebox--nonnegative-horizontal-size-pixels column 0) 0))))
@ -192,6 +199,13 @@
(defun ebox-grid--normalize-config-props (props) (defun ebox-grid--normalize-config-props (props)
"Normalize GridConfig PROPS once to canonical layout-owned facts." "Normalize GridConfig PROPS once to canonical layout-owned facts."
(dolist (property '(:grid-template-columns :grid-template-rows))
(when-let* ((value (plist-get props property)))
(unless (and (proper-list-p value)
value
(not (ebox-grid--track-form-p value)))
(error "Ebox GridConfig %S requires a nonempty track list: %S"
property value))))
(let ((gaps (ebox-grid--gap-pair props))) (let ((gaps (ebox-grid--gap-pair props)))
(list (list
:grid-template-columns :grid-template-columns
@ -221,11 +235,11 @@
ebox--grid-align-items-values :align-items) ebox--grid-align-items-values :align-items)
:justify-content :justify-content
(ebox-grid--enum-value (ebox-grid--enum-value
(plist-get props :justify-content) 'start (plist-get props :justify-content) 'flex-start
ebox--grid-content-alignment-values :justify-content) ebox--grid-content-alignment-values :justify-content)
:align-content :align-content
(ebox-grid--enum-value (ebox-grid--enum-value
(plist-get props :align-content) 'start (plist-get props :align-content) 'stretch
ebox--grid-content-alignment-values :align-content)))) ebox--grid-content-alignment-values :align-content))))
(defconst ebox--grid-default-config-props (defconst ebox--grid-default-config-props
@ -238,6 +252,8 @@
(memq (plist-get track :kind) allowed-kinds) (memq (plist-get track :kind) allowed-kinds)
(pcase (plist-get track :kind) (pcase (plist-get track :kind)
('auto (equal track '(:kind auto :factor 0))) ('auto (equal track '(:kind auto :factor 0)))
('min-content (equal track '(:kind min-content)))
('max-content (equal track '(:kind max-content)))
('fixed ('fixed
(let ((size (plist-get track :size))) (let ((size (plist-get track :size)))
(and (numberp size) (>= size 0) (and (numberp size) (>= size 0)
@ -250,13 +266,15 @@
(defun ebox-grid--canonical-track-p (track) (defun ebox-grid--canonical-track-p (track)
"Return non-nil when TRACK is one canonical top-level track descriptor." "Return non-nil when TRACK is one canonical top-level track descriptor."
(or (ebox-grid--canonical-atomic-track-p track '(fixed auto fr)) (or (ebox-grid--canonical-atomic-track-p
track '(fixed auto min-content max-content fr))
(and (proper-list-p track) (and (proper-list-p track)
(eq (plist-get track :kind) 'minmax) (eq (plist-get track :kind) 'minmax)
(ebox-grid--canonical-atomic-track-p (ebox-grid--canonical-atomic-track-p
(plist-get track :min) '(fixed auto)) (plist-get track :min) '(fixed auto min-content max-content))
(ebox-grid--canonical-atomic-track-p (ebox-grid--canonical-atomic-track-p
(plist-get track :max) '(fixed auto fr)) (plist-get track :max)
'(fixed auto min-content max-content fr))
(equal track (equal track
(list :kind 'minmax (list :kind 'minmax
:min (plist-get track :min) :min (plist-get track :min)
@ -313,8 +331,8 @@
;;;###autoload ;;;###autoload
(defun ebox-grid-layout-create (&rest plist) (defun ebox-grid-layout-create (&rest plist)
"Return a typed GridConfig from canonical evaluated PLIST. "Return a typed GridConfig from canonical evaluated PLIST.
Author `:gap' sugar and legacy grid-gap aliases are normalized before this Author `:gap' sugar is expanded before this programmatic port. Removed
programmatic port and are therefore rejected here." grid-gap aliases are rejected at the author schema boundary."
(ebox-grid--validate-config-input plist) (ebox-grid--validate-config-input plist)
(let ((props (ebox-grid--normalize-config-props plist))) (let ((props (ebox-grid--normalize-config-props plist)))
(unless (ebox-grid-layout-config-props-p props) (unless (ebox-grid-layout-config-props-p props)
@ -345,6 +363,11 @@ programmatic port and are therefore rejected here."
(if (> value 0) (if (> value 0)
(list value 1) (list value 1)
(error "ebox-grid: placement starts at one: %S" value))) (error "ebox-grid: placement starts at one: %S" value)))
((and (proper-list-p value)
(eq (car value) :span)
(integerp (cadr value))
(null (cddr value)))
(list default (cadr value)))
((and (consp value) (numberp (car value))) ((and (consp value) (numberp (car value)))
(let ((start (car value)) (let ((start (car value))
(tail (cdr value))) (tail (cdr value)))
@ -364,8 +387,8 @@ programmatic port and are therefore rejected here."
(let* ((row (ebox-grid--placement-part (plist-get child :grid-row) 0)) (let* ((row (ebox-grid--placement-part (plist-get child :grid-row) 0))
(column (ebox-grid--placement-part (column (ebox-grid--placement-part
(plist-get child :grid-column) 0)) (plist-get child :grid-column) 0))
(row-span (or (plist-get child :grid-row-span) (cadr row))) (row-span (cadr row))
(column-span (or (plist-get child :grid-column-span) (cadr column)))) (column-span (cadr column)))
(dolist (part (list row column)) (dolist (part (list row column))
(when (or (not (integerp (car part))) (< (car part) 0) (when (or (not (integerp (car part))) (< (car part) 0)
(not (integerp (cadr part))) (< (cadr part) 1)) (not (integerp (cadr part))) (< (cadr part) 1))
@ -491,6 +514,29 @@ programmatic port and are therefore rejected here."
(list (nreverse entries) matrix (length matrix) (list (nreverse entries) matrix (length matrix)
(length (aref matrix 0))))) (length (aref matrix 0)))))
(defun ebox-grid--string-min-content-pixel (string)
"Return STRING's widest unbreakable run in pixels."
(let ((best 0))
(dolist (line (ebox-string-lines string) best)
(setq best (max best (ebox--line-min-content-pixel line))))))
(defun ebox-grid--entry-intrinsic-size (entry rendered axis mode)
"Return ENTRY's intrinsic size from RENDERED for AXIS and MODE."
(let* ((node (plist-get entry :node))
(string (gethash node rendered)))
(if (eq axis 'rows)
(ebox-string-height string)
(pcase mode
('min-content
(if (ebox-box-node-p node)
(+ (ebox--side-pixel node) (ebox--content-min-pixel node))
(ebox-grid--string-min-content-pixel string)))
('max-content
(if (ebox-box-node-p node)
(+ (ebox--side-pixel node) (ebox--content-max-pixel node))
(ebox--string-max-pixel-width string)))
(_ (ebox--string-pixel-width string))))))
(defun ebox-grid--track-intrinsic (track axis entries rendered key slot-index) (defun ebox-grid--track-intrinsic (track axis entries rendered key slot-index)
"Return intrinsic size for TRACK from RENDERED grid ENTRIES." "Return intrinsic size for TRACK from RENDERED grid ENTRIES."
(pcase (plist-get track :kind) (pcase (plist-get track :kind)
@ -500,6 +546,7 @@ programmatic port and are therefore rejected here."
(ebox-grid--track-intrinsic (plist-get track :min) (ebox-grid--track-intrinsic (plist-get track :min)
axis entries rendered key slot-index)) axis entries rendered key slot-index))
(_ (let ((position (1+ slot-index)) (_ (let ((position (1+ slot-index))
(mode (plist-get track :kind))
(best 0)) (best 0))
(dolist (entry entries best) (dolist (entry entries best)
(when (and (<= (plist-get entry key) position) (when (and (<= (plist-get entry key) position)
@ -509,11 +556,9 @@ programmatic port and are therefore rejected here."
(if (eq key :column) (if (eq key :column)
:column-span :column-span
:row-span))))) :row-span)))))
(let* ((node (plist-get entry :node)) (let ((value
(string (gethash node rendered)) (ebox-grid--entry-intrinsic-size
(value (if (eq axis 'columns) entry rendered axis mode)))
(ebox--string-pixel-width string)
(ebox-string-height string))))
(setq best (max best value))))))))) (setq best (max best value)))))))))
(defun ebox-grid--sum (values) (defun ebox-grid--sum (values)

View File

@ -6598,12 +6598,10 @@ recognized from its first untouched sibling without walking the whole tree."
(defun ebox-incremental--declaration-changed-keys (old new) (defun ebox-incremental--declaration-changed-keys (old new)
"Return engine longhands changed between canonical OLD and NEW declarations." "Return engine longhands changed between canonical OLD and NEW declarations."
(let ((all-properties (lambda (_property) t))) (delete-dups
(ebox-incremental--plist-changed-keys (cl-mapcan
(ebox-style-expand-ebox-plist #'ebox-style--engine-projection-targets
(ebox-style-declaration-properties old all-properties)) (ebox-incremental--plist-changed-keys old new))))
(ebox-style-expand-ebox-plist
(ebox-style-declaration-properties new all-properties)))))
(defun ebox-incremental--candidate-source-record (source-index node) (defun ebox-incremental--candidate-source-record (source-index node)
"Return NODE's borrowed immutable record from candidate SOURCE-INDEX. "Return NODE's borrowed immutable record from candidate SOURCE-INDEX.
@ -9507,8 +9505,8 @@ role, and outside-complement compatibility before publication."
(or (eq key :content) (or (eq key :content)
(eq key :surface-properties) (eq key :surface-properties)
(memq key ebox--paint-style-signature-keys) (memq key ebox--paint-style-signature-keys)
(memq key '(:font :font-family :font-height (memq key '(:font-family :font-size
:font-weight :font-slant)))) :font-weight :font-style))))
(plist-get entry :changed-keys)))) (plist-get entry :changed-keys))))
dirty-set) dirty-set)
allocation-stable-p allocation-stable-p

View File

@ -124,7 +124,8 @@ PROPS must already use the variant's normalized representation."
:grid-auto-columns :grid-auto-rows :grid-auto-flow :grid-auto-columns :grid-auto-rows :grid-auto-flow
:row-gap :column-gap :row-gap :column-gap
:justify-items :align-items :justify-content :align-content)) :justify-items :align-items :justify-content :align-content))
((or 'box 'row 'column) nil) ('box nil)
((or 'row 'column) '(:item-gap :cross-align))
(_ (error "Unknown Ebox Box author form: %S" tag)))) (_ (error "Unknown Ebox Box author form: %S" tag))))
(defun ebox-layout-config-for-form (tag properties) (defun ebox-layout-config-for-form (tag properties)

View File

@ -58,8 +58,12 @@
"ebox-buffer-backend" (string property region-id)) "ebox-buffer-backend" (string property region-id))
(declare-function ebox--propertize-typography (declare-function ebox--propertize-typography
"ebox-buffer-backend" (string style)) "ebox-buffer-backend" (string style))
(declare-function ebox--propertize-text-decoration
"ebox-buffer-backend" (string style))
(declare-function ebox--propertize-underline (declare-function ebox--propertize-underline
"ebox-buffer-backend" (string &optional color)) "ebox-buffer-backend" (string &optional color))
(declare-function ebox-buffer-validate-border-capability
"ebox-buffer-backend" (style))
(defvar ebox--render-runtime-revision nil (defvar ebox--render-runtime-revision nil
"Current buffer runtime revision for render-local cache validation.") "Current buffer runtime revision for render-local cache validation.")
@ -230,8 +234,8 @@ the generic box pipeline."
(equal (ebox-get box :margin-bottom-height) 0) (equal (ebox-get box :margin-bottom-height) 0)
(equal (ebox-get box :border-left-pixel) 0) (equal (ebox-get box :border-left-pixel) 0)
(equal (ebox-get box :border-right-pixel) 0) (equal (ebox-get box :border-right-pixel) 0)
(not (ebox-get box :border-top-p)) (= (ebox-get box :border-top-pixel) 0)
(not (ebox-get box :border-bottom-p)) (= (ebox-get box :border-bottom-pixel) 0)
(null (and (ebox-style--text-paint-owner-p box) (null (and (ebox-style--text-paint-owner-p box)
(ebox-get box :color))) (ebox-get box :color)))
(null (ebox-get box :bgcolor))) (null (ebox-get box :bgcolor)))
@ -296,8 +300,8 @@ and its formatted content exactly fills the used height."
(= (floor (ebox-get box :padding-bottom-height)) 0) (= (floor (ebox-get box :padding-bottom-height)) 0)
(= (ebox-get box :border-left-pixel) 0) (= (ebox-get box :border-left-pixel) 0)
(= (ebox-get box :border-right-pixel) 0) (= (ebox-get box :border-right-pixel) 0)
(not (ebox-get box :border-top-p)) (= (ebox-get box :border-top-pixel) 0)
(not (ebox-get box :border-bottom-p))))) (= (ebox-get box :border-bottom-pixel) 0))))
(when (and lines-fit-p (when (and lines-fit-p
(or (= text-height content-height) (or (= text-height content-height)
underfilled-simple-scroll-p)) underfilled-simple-scroll-p))
@ -1203,7 +1207,8 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
;; typography contribution around already-rendered descendants. ;; typography contribution around already-rendered descendants.
(if (eq (plist-get box :ebox-kind) 'box) (if (eq (plist-get box :ebox-kind) 'box)
content content
(ebox--propertize-typography content box)))) (ebox--propertize-text-decoration
(ebox--propertize-typography content box) box))))
(defun ebox--intrinsic-box-content (box) (defun ebox--intrinsic-box-content (box)
"Return BOX content materialized independently of its used width. "Return BOX content materialized independently of its used width.
@ -1556,9 +1561,9 @@ Internal implementation of `ebox-render' for box nodes."
(border-left-color (ebox-get box :border-left-color)) (border-left-color (ebox-get box :border-left-color))
(border-right-pixel (ebox-get box :border-right-pixel)) (border-right-pixel (ebox-get box :border-right-pixel))
(border-right-color (ebox-get box :border-right-color)) (border-right-color (ebox-get box :border-right-color))
(border-top-p (ebox-get box :border-top-p)) (border-top-p (> (ebox-get box :border-top-pixel) 0))
(border-top-color (ebox-get box :border-top-color)) (border-top-color (ebox-get box :border-top-color))
(border-bottom-p (ebox-get box :border-bottom-p)) (border-bottom-p (> (ebox-get box :border-bottom-pixel) 0))
(border-bottom-color (ebox-get box :border-bottom-color)) (border-bottom-color (ebox-get box :border-bottom-color))
;; Style and Layout ;; Style and Layout
@ -1887,7 +1892,9 @@ Internal implementation of `ebox-render' for box nodes."
(let* ((region-id (or ebox--render-region-id (let* ((region-id (or ebox--render-region-id
(ebox--ensure-region-id node))) (ebox--ensure-region-id node)))
(value (copy-sequence (plist-get node :ebox-text-value))) (value (copy-sequence (plist-get node :ebox-text-value)))
(styled (ebox--propertize-typography value node)) (styled
(ebox--propertize-text-decoration
(ebox--propertize-typography value node) node))
(painted (painted
(if (or (ebox-get node :color) (ebox-get node :bgcolor)) (if (or (ebox-get node :color) (ebox-get node :bgcolor))
(ebox--propertize-colors (ebox--propertize-colors
@ -1921,7 +1928,8 @@ This function owns spatial layout only; it never creates a live TP surface."
(or ebox--box-content-render-cache (or ebox--box-content-render-cache
(make-hash-table :test 'eq)))) (make-hash-table :test 'eq))))
(when (listp node) (when (listp node)
(ebox--ensure-node-id node)) (ebox--ensure-node-id node)
(ebox-buffer-validate-border-capability node))
(let ((type (plist-get node :ebox-type))) (let ((type (plist-get node :ebox-type)))
(cond (cond
((eq (plist-get node :ebox-kind) 'text) ((eq (plist-get node :ebox-kind) 'text)
@ -2815,15 +2823,12 @@ last cached leaf instead of walking the stack from the beginning."
:margin-left-pixel :margin-right-pixel :margin-left-pixel :margin-right-pixel
:margin-top-height :margin-bottom-height :margin-top-height :margin-bottom-height
:border-left-pixel :border-right-pixel)) :border-left-pixel :border-right-pixel))
(not (ebox-get node :border-top-p)) (= (ebox-get node :border-top-pixel) 0)
(not (ebox-get node :border-bottom-p)) (= (ebox-get node :border-bottom-pixel) 0)
(null (ebox-get node :color)) (null (ebox-get node :color))
(null (ebox-get node :bgcolor)) (null (ebox-get node :bgcolor))
(null (ebox-get node :font)) (or (null (plist-get node :ebox-font-fact))
(null (ebox-get node :font-family)) (ebox-font-fact-neutral-p (plist-get node :ebox-font-fact)))
(null (ebox-get node :font-height))
(null (ebox-get node :font-weight))
(null (ebox-get node :font-slant))
(null (ebox-get node :surface-properties)) (null (ebox-get node :surface-properties))
(eq (ebox-get node :visibility) 'visible) (eq (ebox-get node :visibility) 'visible)
(eq (ebox-get node :text-align) 'left) (eq (ebox-get node :text-align) 'left)
@ -2951,10 +2956,11 @@ no included vertical margin."
0)) 0))
(border-left-pixel (ebox-get box :border-left-pixel)) (border-left-pixel (ebox-get box :border-left-pixel))
(border-right-pixel (ebox-get box :border-right-pixel)) (border-right-pixel (ebox-get box :border-right-pixel))
(border-top-p (and include-top (ebox-get box :border-top-p))) (border-top-p
(and include-top (> (ebox-get box :border-top-pixel) 0)))
(border-top-color (ebox-get box :border-top-color)) (border-top-color (ebox-get box :border-top-color))
(border-bottom-p (border-bottom-p
(and include-bottom (ebox-get box :border-bottom-p))) (and include-bottom (> (ebox-get box :border-bottom-pixel) 0)))
(border-bottom-color (ebox-get box :border-bottom-color)) (border-bottom-color (ebox-get box :border-bottom-color))
(color (and (ebox-style--text-paint-owner-p box) (color (and (ebox-style--text-paint-owner-p box)
(ebox-get box :color))) (ebox-get box :color)))
@ -3128,7 +3134,7 @@ only used for incomplete lazy prefixes."
(border-right-pixel (ebox-get box :border-right-pixel)) (border-right-pixel (ebox-get box :border-right-pixel))
(border-right-color (ebox-get box :border-right-color)) (border-right-color (ebox-get box :border-right-color))
(border-top-p (and include-top (border-top-p (and include-top
(ebox-get box :border-top-p))) (> (ebox-get box :border-top-pixel) 0)))
(border-top-color (ebox-get box :border-top-color)) (border-top-color (ebox-get box :border-top-color))
(color (and (ebox-style--text-paint-owner-p box) (color (and (ebox-style--text-paint-owner-p box)
(ebox-get box :color))) (ebox-get box :color)))

View File

@ -8,6 +8,7 @@
(require 'cl-lib) (require 'cl-lib)
(require 'ebox-cache) (require 'ebox-cache)
(require 'ebox-font)
(declare-function ebox--register-render-owned-text-value (declare-function ebox--register-render-owned-text-value
"ebox-render-context" (property value)) "ebox-render-context" (property value))
@ -251,21 +252,29 @@ switching those layers must not invalidate Ebox measurement caches."
This is the public, text-scale-aware measurement used by Ebox layout code." This is the public, text-scale-aware measurement used by Ebox layout code."
(ebox--string-pixel-width string)) (ebox--string-pixel-width string))
(defun ebox--display-signature () (defun ebox--display-signature-for-frame (frame)
"Return the current display signature for pixel measurement caches." "Return the pixel-measurement capability signature for FRAME."
(list (frame-parameter nil 'font) (list frame
(frame-parameter nil 'font-backend) (frame-parameter frame 'font)
(frame-parameter nil 'line-spacing) (frame-parameter frame 'font-backend)
(frame-parameter nil 'internal-border-width) (frame-parameter frame 'line-spacing)
(face-attribute 'default :family nil t) (face-attribute 'default :family frame t)
(face-attribute 'default :height nil t) (face-attribute 'default :height frame t)
(face-attribute 'default :weight nil t) (face-attribute 'default :weight frame t)
(face-attribute 'default :slant nil t) (face-attribute 'default :slant frame t)
(face-attribute 'default :width nil t) (face-attribute 'default :width frame t)
(face-attribute 'default :foreground nil t)
(face-attribute 'default :background nil t)
(ebox--measurement-face-remapping) (ebox--measurement-face-remapping)
(and (boundp 'text-scale-mode-amount) text-scale-mode-amount))) (and (boundp 'text-scale-mode-amount) text-scale-mode-amount)
(and (boundp 'text-scale-mode-step) text-scale-mode-step)))
(defun ebox--display-signature-for-window (window)
"Return the pixel-measurement capability signature for live WINDOW."
(ebox--display-signature-for-frame
(if (window-live-p window) (window-frame window) (selected-frame))))
(defun ebox--display-signature ()
"Return the selected frame's pixel-measurement capability signature."
(ebox--display-signature-for-frame (selected-frame)))
(defsubst ebox--current-display-signature () (defsubst ebox--current-display-signature ()
"Return the render snapshot or sample the current display signature." "Return the render snapshot or sample the current display signature."
@ -336,6 +345,7 @@ Nested uses reuse the outer validation."
"Clear pixel width caches after font or named face metrics change." "Clear pixel width caches after font or named face metrics change."
(clrhash ebox--char-width-cache) (clrhash ebox--char-width-cache)
(ebox--clear-string-pixel-width-cache) (ebox--clear-string-pixel-width-cache)
(ebox-font-clear-cache)
(setq ebox--space-pixel-cache nil) (setq ebox--space-pixel-cache nil)
(setq ebox--face-height-width-cache nil) (setq ebox--face-height-width-cache nil)
(setq ebox--display-signature-cache (ebox--display-signature))) (setq ebox--display-signature-cache (ebox--display-signature)))

View File

@ -53,6 +53,8 @@
(declare-function ebox-layout-config-props "ebox-layout-config" (config)) (declare-function ebox-layout-config-props "ebox-layout-config" (config))
(declare-function ebox-tree-node-children "ebox-tree" (node)) (declare-function ebox-tree-node-children "ebox-tree" (node))
(declare-function ebox-buffer--font-face "ebox-buffer-backend" (style)) (declare-function ebox-buffer--font-face "ebox-buffer-backend" (style))
(declare-function ebox--propertize-typography
"ebox-buffer-backend" (string style))
(declare-function ebox--ensure-node-id "ebox" (node)) (declare-function ebox--ensure-node-id "ebox" (node))
(declare-function ebox--buffer-render-state "ebox-incremental" (buffer)) (declare-function ebox--buffer-render-state "ebox-incremental" (buffer))
(declare-function ebox--ensure-region-id "ebox" (box)) (declare-function ebox--ensure-region-id "ebox" (box))
@ -77,7 +79,11 @@
(declare-function ebox-pixel-space "ebox-measure" (pixel-width)) (declare-function ebox-pixel-space "ebox-measure" (pixel-width))
(declare-function ebox-string-lines "ebox" (string)) (declare-function ebox-string-lines "ebox" (string))
(declare-function ebox-string-pixel-width "ebox-measure" (string)) (declare-function ebox-string-pixel-width "ebox-measure" (string))
(declare-function ebox-display-signature "ebox-measure" ()) (declare-function ebox--current-display-signature "ebox-measure" ())
(declare-function ebox--display-signature-for-window
"ebox-measure" (window))
(declare-function ebox-surface--buffer-display-window
"ebox-surface" (buffer))
(declare-function tp-property-value-copy "tp-core" (value)) (declare-function tp-property-value-copy "tp-core" (value))
(declare-function tp-paint-slot-p "tp-style" (value)) (declare-function tp-paint-slot-p "tp-style" (value))
(declare-function tp-paint-slot-face "tp-style" (slot)) (declare-function tp-paint-slot-face "tp-style" (slot))
@ -1640,8 +1646,8 @@ Rust still verifies the dynamic equal-width and nonempty-line proof per frame."
:margin-left-pixel :margin-right-pixel :margin-left-pixel :margin-right-pixel
:margin-top-height :margin-bottom-height :margin-top-height :margin-bottom-height
:border-left-pixel :border-right-pixel)) :border-left-pixel :border-right-pixel))
(not (ebox-get box :border-top-p)) (= (ebox-get box :border-top-pixel) 0)
(not (ebox-get box :border-bottom-p)) (= (ebox-get box :border-bottom-pixel) 0)
(null (ebox-get box :color)) (null (ebox-get box :color))
(null (ebox-get box :bgcolor)))) (null (ebox-get box :bgcolor))))
@ -1743,9 +1749,16 @@ cross-axis stretch and item gap remain explicit without legacy runtime nodes."
(defun ebox-native-reflow--compile-text-node (node) (defun ebox-native-reflow--compile-text-node (node)
"Compile canonical Text NODE to the lightweight native Text IR." "Compile canonical Text NODE to the lightweight native Text IR."
(let* ((region-id (ebox--ensure-region-id node)) (let* ((region-id (ebox--ensure-region-id node))
(content
(ebox--propertize-typography
(or (plist-get node :ebox-text-value)
(ebox-get node :content)
"")
node))
(typography-style (typography-style
(ebox-native-reflow--compile-add-face-style (ebox-native-reflow--compile-add-face-style
(ebox-buffer--font-face node))) (append (ebox-buffer--font-face node)
(ebox-buffer--text-decoration-face node))))
(foreground-style (foreground-style
(ebox-native-reflow--compile-add-face-style (ebox-native-reflow--compile-add-face-style
(ebox-native-reflow--compile-foreground-face (ebox-native-reflow--compile-foreground-face
@ -1757,10 +1770,7 @@ cross-axis stretch and item gap remain explicit without legacy runtime nodes."
(list :type "text" (list :type "text"
:region-id region-id :region-id region-id
:content :content
(ebox-native-reflow--compile-text (ebox-native-reflow--compile-text content)
(or (plist-get node :ebox-text-value)
(ebox-get node :content)
""))
:typography-style (or typography-style :null) :typography-style (or typography-style :null)
:foreground-style (or foreground-style :null) :foreground-style (or foreground-style :null)
:surface-template-id (or surface-template-id :null) :surface-template-id (or surface-template-id :null)
@ -1800,6 +1810,14 @@ cross-axis stretch and item gap remain explicit without legacy runtime nodes."
(margin-bottom (floor (or (ebox-get box :margin-bottom-height) 0))) (margin-bottom (floor (or (ebox-get box :margin-bottom-height) 0)))
(border-left (floor (or (ebox-get box :border-left-pixel) 0))) (border-left (floor (or (ebox-get box :border-left-pixel) 0)))
(border-right (floor (or (ebox-get box :border-right-pixel) 0))) (border-right (floor (or (ebox-get box :border-right-pixel) 0)))
(border-top (floor (or (ebox-get box :border-top-pixel) 0)))
(border-bottom (floor (or (ebox-get box :border-bottom-pixel) 0)))
(_border-capability
(when (or (> border-top 1) (> border-bottom 1))
(signal 'ebox-surface-capability-error
(list :border '(top bottom)
:declared-pixels (max border-top border-bottom)
:supported-pixels '(0 1)))))
(foreground-style (foreground-style
(ebox-native-reflow--compile-add-face-style (ebox-native-reflow--compile-add-face-style
(ebox-native-reflow--compile-foreground-face (ebox-native-reflow--compile-foreground-face
@ -1808,7 +1826,8 @@ cross-axis stretch and item gap remain explicit without legacy runtime nodes."
(typography-style (typography-style
(and (ebox-style--text-paint-owner-p box) (and (ebox-style--text-paint-owner-p box)
(ebox-native-reflow--compile-add-face-style (ebox-native-reflow--compile-add-face-style
(ebox-buffer--font-face box)))) (append (ebox-buffer--font-face box)
(ebox-buffer--text-decoration-face box)))))
(background-style (background-style
(ebox-native-reflow--compile-add-face-style (ebox-native-reflow--compile-add-face-style
(let ((background (ebox-get box :bgcolor))) (let ((background (ebox-get box :bgcolor)))
@ -1823,14 +1842,14 @@ cross-axis stretch and item gap remain explicit without legacy runtime nodes."
(ebox-native-reflow--compile-border-face-style (ebox-native-reflow--compile-border-face-style
border-right (ebox-get box :border-right-color))) border-right (ebox-get box :border-right-color)))
(border-top-style (border-top-style
(and (ebox-get box :border-top-p) (and (> border-top 0)
(ebox-native-reflow--compile-add-face-style (ebox-native-reflow--compile-add-face-style
(let ((color (ebox-get box :border-top-color))) (let ((color (ebox-get box :border-top-color)))
(if (tp-paint-slot-p color) (if (tp-paint-slot-p color)
(tp-paint-slot-face color) (tp-paint-slot-face color)
(list :overline (or color t))))))) (list :overline (or color t)))))))
(border-bottom-style (border-bottom-style
(and (ebox-get box :border-bottom-p) (and (> border-bottom 0)
(ebox-native-reflow--compile-add-face-style (ebox-native-reflow--compile-add-face-style
(let ((color (ebox-get box :border-bottom-color))) (let ((color (ebox-get box :border-bottom-color)))
(if (tp-paint-slot-p color) (if (tp-paint-slot-p color)
@ -2220,6 +2239,12 @@ PROPERTY-TEMPLATES are Emacs-owned plists addressed by opaque native ids."
:styles (vconcat styles) :styles (vconcat styles)
:property-templates (vconcat property-templates))) :property-templates (vconcat property-templates)))
(defun ebox-native-reflow--buffer-display-signature (buffer)
"Return BUFFER's live canonical Surface display capability."
(with-current-buffer buffer
(ebox--display-signature-for-window
(ebox-surface--buffer-display-window buffer))))
(defun ebox-native-reflow-capture-layout-scene (buffer) (defun ebox-native-reflow-capture-layout-scene (buffer)
"Capture BUFFER's render-owned flat native scene without walking its tree." "Capture BUFFER's render-owned flat native scene without walking its tree."
(unless (buffer-live-p buffer) (unless (buffer-live-p buffer)
@ -2238,7 +2263,7 @@ PROPERTY-TEMPLATES are Emacs-owned plists addressed by opaque native ids."
:node-postorder postorder :node-postorder postorder
:flex-content-min-widths :flex-content-min-widths
(plist-get state :flex-content-min-widths) (plist-get state :flex-content-min-widths)
:display-signature (ebox-display-signature) :display-signature (plist-get state :display-signature)
:runtime-revision (or (plist-get state :runtime-revision) 0))))) :runtime-revision (or (plist-get state :runtime-revision) 0)))))
(defun ebox-native-reflow-layout-scene-new-builder (scene) (defun ebox-native-reflow-layout-scene-new-builder (scene)
@ -2264,10 +2289,8 @@ PROPERTY-TEMPLATES are Emacs-owned plists addressed by opaque native ids."
(and (buffer-live-p buffer) (and (buffer-live-p buffer)
(eq (ebox--buffer-render-state buffer) (eq (ebox--buffer-render-state buffer)
(ebox-native-reflow-layout-scene-render-state scene)) (ebox-native-reflow-layout-scene-render-state scene))
(with-current-buffer buffer (equal (ebox-native-reflow--buffer-display-signature buffer)
(equal (ebox-display-signature) (ebox-native-reflow-layout-scene-display-signature scene)))))
(ebox-native-reflow-layout-scene-display-signature
scene))))))
(defun ebox-native-reflow-layout-builder-step (builder budget-seconds) (defun ebox-native-reflow-layout-builder-step (builder budget-seconds)
"Advance flat layout BUILDER for at most BUDGET-SECONDS. "Advance flat layout BUILDER for at most BUDGET-SECONDS.
@ -2359,7 +2382,7 @@ per call, and no call recursively visits the captured Ebox tree."
(ebox-native-reflow--compile-property-template-ids (ebox-native-reflow--compile-property-template-ids
(make-hash-table :test 'equal)) (make-hash-table :test 'equal))
(ebox-native-reflow--compile-display-signature (ebox-native-reflow--compile-display-signature
(ebox-display-signature)) (ebox--current-display-signature))
(ebox-native-reflow--compile-root-node node)) (ebox-native-reflow--compile-root-node node))
(cl-loop (cl-loop
for template in ebox-native-reflow--compile-property-templates for template in ebox-native-reflow--compile-property-templates
@ -2457,7 +2480,7 @@ per call, and no call recursively visits the captured Ebox tree."
(ebox-native-reflow--compile-property-template-ids (ebox-native-reflow--compile-property-template-ids
(make-hash-table :test 'equal)) (make-hash-table :test 'equal))
(ebox-native-reflow--compile-display-signature (ebox-native-reflow--compile-display-signature
(ebox-display-signature)) (ebox--current-display-signature))
(ebox-native-reflow--compile-root-node node) (ebox-native-reflow--compile-root-node node)
(ebox--render-string-pixel-width-cache (ebox--render-string-pixel-width-cache
(make-hash-table :test 'equal)) (make-hash-table :test 'equal))
@ -4353,8 +4376,7 @@ creating and releasing a session for this preparation."
(or layout-package (or layout-package
(ebox-native-reflow--compile-layout-package root))) (ebox-native-reflow--compile-layout-package root)))
(source-revision (or (plist-get state :runtime-revision) 0)) (source-revision (or (plist-get state :runtime-revision) 0))
(display-signature (display-signature (plist-get state :display-signature))
(with-current-buffer buffer (ebox-display-signature)))
(context-hash (context-hash
(sxhash-equal (sxhash-equal
(list display-signature kind region-id (list display-signature kind region-id
@ -4505,11 +4527,10 @@ When FULL-P is non-nil, query the complete restoration job."
(equal (equal
(plist-get state :runtime-revision) (plist-get state :runtime-revision)
(ebox-native-reflow-preparation-expected-revision preparation)) (ebox-native-reflow-preparation-expected-revision preparation))
(with-current-buffer buffer (equal
(equal (ebox-native-reflow--buffer-display-signature buffer)
(ebox-display-signature) (ebox-native-reflow-preparation-display-signature
(ebox-native-reflow-preparation-display-signature preparation)))
preparation))))
(error "Preview runtime changed during native reflow preparation")) (error "Preview runtime changed during native reflow preparation"))
state)) state))

View File

@ -18,18 +18,32 @@
:padding-top-height 0 :padding-bottom-height 0 :padding-top-height 0 :padding-bottom-height 0
:margin-left-pixel 0 :margin-right-pixel 0 :margin-left-pixel 0 :margin-right-pixel 0
:margin-top-height 0 :margin-bottom-height 0 :margin-top-height 0 :margin-bottom-height 0
:border-left-pixel 0 :border-left-color nil :border-left-pixel 0 :border-left-style none :border-left-color nil
:border-right-pixel 0 :border-right-color nil :border-right-pixel 0 :border-right-style none :border-right-color nil
:border-top-p nil :border-top-color nil :border-top-pixel 0 :border-top-style none :border-top-color nil
:border-bottom-p nil :border-bottom-color nil :border-bottom-pixel 0 :border-bottom-style none :border-bottom-color nil
:color nil :bgcolor nil :color nil :bgcolor nil
:font nil :font-family nil :font-height nil :text-decoration-line none :text-decoration-color currentColor
:font-weight nil :font-slant nil :text-decoration-style solid
:text-align left :vertical-align top :text-align left :vertical-align top
:overflow scroll :visibility visible :overflow scroll :visibility visible
:wrap-mode word :scroll-offset 0) :wrap-mode word :scroll-offset 0)
"Default longhand fields for the current runtime box encoding.") "Default longhand fields for the current runtime box encoding.")
(defconst ebox-node-factory--style-projection-keys
(let ((fields ebox--longhand)
keys)
(while fields
(let ((key (pop fields)))
(pop fields)
(unless (memq key '(:content :scroll-offset))
(push key keys))))
(nreverse keys))
"Private runtime fields materialized from canonical style facts.
These fields are projections, never declarative source authority. Content and
scroll offset remain outside this set because their lifecycle is not owned by
style projection.")
(defconst ebox-node-factory--text-longhand (defconst ebox-node-factory--text-longhand
'(:content "" '(:content ""
:overflow visible :overflow visible
@ -44,7 +58,10 @@
:padding-top-height 0 :padding-bottom-height 0 :padding-top-height 0 :padding-bottom-height 0
:margin-left-pixel 0 :margin-right-pixel 0 :margin-left-pixel 0 :margin-right-pixel 0
:margin-top-height 0 :margin-bottom-height 0 :margin-top-height 0 :margin-bottom-height 0
:border-left-pixel 0 :border-right-pixel 0 :border-left-pixel 0 :border-left-style none
:border-right-pixel 0 :border-right-style none
:border-top-pixel 0 :border-top-style none
:border-bottom-pixel 0 :border-bottom-style none
:text-align left :vertical-align top :text-align left :vertical-align top
:overflow scroll :visibility visible :overflow scroll :visibility visible
:wrap-mode word :scroll-offset 0) :wrap-mode word :scroll-offset 0)
@ -87,7 +104,7 @@ leaving them absent would make the canonical node violate the Box contract.")
(defun ebox-node-factory--create-from-properties (&rest plist) (defun ebox-node-factory--create-from-properties (&rest plist)
"Create one private runtime box from evaluated engine PLIST." "Create one private runtime box from evaluated engine PLIST."
(ebox-node-factory--create-expanded (ebox-node-factory--create-expanded
(ebox-style-expand-ebox-plist plist))) (ebox-style--expand-engine-plist plist)))
(provide 'ebox-node-factory) (provide 'ebox-node-factory)

View File

@ -216,6 +216,23 @@ Functions and records are opaque values and retain identity."
copy)) copy))
(t value))) (t value)))
(defun ebox-source-stable-key-p (value)
"Return non-nil when VALUE is one public stable source key."
(and value (or (symbolp value) (stringp value) (integerp value))))
(defun ebox-source-semantic-id-p (value)
"Return non-nil when VALUE is one public semantic id."
(and value (or (symbolp value) (stringp value) (integerp value))))
(defun ebox-source-class-list-p (value)
"Return non-nil when VALUE is a public class token or proper token list."
(or (symbolp value)
(stringp value)
(and (proper-list-p value)
(cl-every (lambda (token)
(or (symbolp token) (stringp token)))
value))))
(defun ebox-source--metadata-string (value) (defun ebox-source--metadata-string (value)
"Return VALUE normalized to a selector metadata string." "Return VALUE normalized to a selector metadata string."
(cond (cond
@ -299,6 +316,15 @@ Functions and records are opaque values and retain identity."
"Bind normalized source facts in BUILDER and return an opaque handle. "Bind normalized source facts in BUILDER and return an opaque handle.
The returned handle contains identity only; all facts are owned by BUILDER The returned handle contains identity only; all facts are owned by BUILDER
until `ebox-source-builder-finish' seals them into a source index." until `ebox-source-builder-finish' seals them into a source index."
(when (and key (not (ebox-source-stable-key-p key)))
(error "Ebox source key must be a non-nil symbol, string, or integer: %S"
key))
(when (and id (not (ebox-source-semantic-id-p id)))
(error "Ebox source id must be a non-nil symbol, string, or integer: %S"
id))
(unless (ebox-source-class-list-p class)
(error "Ebox source class must be a token or proper token list: %S"
class))
(let ((handle (let ((handle
(ebox-source--make-handle (ebox-source--make-handle
:identity (ebox-source--copy-data :identity (ebox-source--copy-data

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@
(require 'cl-lib) (require 'cl-lib)
(require 'seq) (require 'seq)
(require 'ebox-buffer-backend) (require 'ebox-buffer-backend)
(require 'ebox-font)
(require 'ebox-style) (require 'ebox-style)
(require 'ebox-tree) (require 'ebox-tree)
(require 'ebox-layout) (require 'ebox-layout)
@ -49,6 +50,9 @@
(defvar ebox-surface--inline-style-value-cache nil (defvar ebox-surface--inline-style-value-cache nil
"Per-projection cache for selector-free computed inline styles.") "Per-projection cache for selector-free computed inline styles.")
(defvar ebox-surface--font-window nil
"Canonical target window frozen for one Surface projection.")
(defun ebox-surface--mixed-range-splice-p (state) (defun ebox-surface--mixed-range-splice-p (state)
"Return non-nil when STATE's mixed proof contains one Range splice." "Return non-nil when STATE's mixed proof contains one Range splice."
(cl-some (cl-some
@ -633,7 +637,8 @@ and resize paths must consume this same choice."
:display-signature :display-signature
(value :display-signature (value :display-signature
(with-current-buffer buffer (with-current-buffer buffer
(ebox--current-display-signature))) (ebox--display-signature-for-window window)))
:display-window window
:scroll-offsets :scroll-offsets
(ebox-surface--scroll-offsets scroll-table))))) (ebox-surface--scroll-offsets scroll-table)))))
@ -1025,23 +1030,25 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
success) success)
(unwind-protect (unwind-protect
(progn (progn
(tp-with-transaction (let ((ebox-surface--font-window
(ebox-surface--stage-signal-values signals context-values) (plist-get context-values :display-window)))
(if surface (tp-with-transaction
(tp-surface-update surface producer) (ebox-surface--stage-signal-values signals context-values)
(setq surface (if surface
(tp-surface-mount (tp-surface-update surface producer)
buffer producer (setq surface
(append (tp-surface-mount
'(:capability content :inhibit-read-only t buffer producer
:coordinate-mounts t) (append
(when-let* ((bridge '(:capability content :inhibit-read-only t
(with-current-buffer buffer :coordinate-mounts t)
ebox-surface--tp-observer))) (when-let* ((bridge
(list :observers (list bridge))))))) (with-current-buffer buffer
(ebox-surface--publish-runtime-state ebox-surface--tp-observer)))
buffer surface old-state report-base after-publication (list :observers (list bridge)))))))
framework-participant observation-context)) (ebox-surface--publish-runtime-state
buffer surface old-state report-base after-publication
framework-participant observation-context)))
(setq success t) (setq success t)
(let ((scroll-keys (let ((scroll-keys
(if framework-participant (if framework-participant
@ -1124,9 +1131,10 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
(let* ((old-width (plist-get state :viewport-width)) (let* ((old-width (plist-get state :viewport-width))
(old-height (plist-get state :viewport-height)) (old-height (plist-get state :viewport-height))
(target-height (or height old-height)) (target-height (or height old-height))
(window (ebox-surface--buffer-display-window buffer))
(display-signature (display-signature
(with-current-buffer buffer (with-current-buffer buffer
(ebox--current-display-signature))) (ebox--display-signature-for-window window)))
(display-changed (display-changed
(not (equal display-signature (not (equal display-signature
(plist-get state :display-signature)))) (plist-get state :display-signature))))
@ -1859,14 +1867,16 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
(setq on-mismatch 'root)) (setq on-mismatch 'root))
(unwind-protect (unwind-protect
(progn (progn
(tp-with-transaction (let ((ebox-surface--font-window
(ebox-surface--stage-signal-values signals context-values) (plist-get context-values :display-window)))
(tp-surface-update-scoped (tp-with-transaction
surface objects producer (ebox-surface--stage-signal-values signals context-values)
(list :on-mismatch on-mismatch :return-report nil)) (tp-surface-update-scoped
(ebox-surface--publish-runtime-state surface objects producer
buffer surface old-state report-base after-publication (list :on-mismatch on-mismatch :return-report nil))
framework-participant observation-context)) (ebox-surface--publish-runtime-state
buffer surface old-state report-base after-publication
framework-participant observation-context)))
(setq success t) (setq success t)
(let ((scroll-keys (let ((scroll-keys
(if framework-participant (if framework-participant
@ -2043,6 +2053,7 @@ published generation so a failed transaction has no cache state to restore."
(plist-put reused-state :selector-tree-token (plist-put reused-state :selector-tree-token
selector-tree-token) selector-tree-token)
(plist-put reused-state :subject subject) (plist-put reused-state :subject subject)
(plist-put reused-state :node node)
(plist-put reused-state :parent-style-state (plist-put reused-state :parent-style-state
parent-style-state) parent-style-state)
reused-state) reused-state)
@ -2058,7 +2069,8 @@ published generation so a failed transaction has no cache state to restore."
0 0
(if parent-binding 1 0))) (if parent-binding 1 0)))
:stylesheet-signature stylesheet-signature :stylesheet-signature stylesheet-signature
:subject subject))) :subject subject
:node node)))
(plist-put (plist-put
new-state :compute new-state :compute
(let ((captured new-state) (let ((captured new-state)
@ -2142,6 +2154,38 @@ published generation so a failed transaction has no cache state to restore."
(puthash key style ebox-surface--inline-style-value-cache) (puthash key style ebox-surface--inline-style-value-cache)
style))))) style)))))
(defun ebox-surface--project-font
(node style &optional parent-font-fact snapshot)
"Install NODE's font fact from STYLE, capability, and PARENT-FONT-FACT."
(let ((snapshot (or snapshot (ebox-style--computed-snapshot style t))))
(plist-put
node :ebox-font-fact
(ebox-font-project-values
(car snapshot) (nth 2 snapshot)
(ebox--current-display-signature)
(or ebox-surface--font-window
(ebox-surface--buffer-display-window (current-buffer)))
parent-font-fact)))
node)
(defun ebox-surface--refresh-font-projections (root)
"Re-resolve retained computed font facts below candidate ROOT.
This is a host-capability projection only: it reuses each immutable computed
style and never re-runs selector matching or cascade."
(cl-labels
((visit (node parent-font-fact)
(when (and (listp node) (not (stringp node)))
(when-let* ((style (plist-get node :ebox-computed-style))
((ecss-computed-style-p style)))
(ebox-surface--project-font
node style parent-font-fact
(ebox-style--computed-snapshot style t)))
(let ((fact (plist-get node :ebox-font-fact)))
(ebox-tree-for-each-direct-child
node (lambda (child) (visit child fact)))))))
(visit root nil))
root)
(defun ebox-surface--closed-canonical-node-p (source-index node) (defun ebox-surface--closed-canonical-node-p (source-index node)
"Return non-nil when NODE in SOURCE-INDEX needs closed inheritance." "Return non-nil when NODE in SOURCE-INDEX needs closed inheritance."
(let ((declarations (let ((declarations
@ -2260,18 +2304,27 @@ source-handle-to-subject table when the requested local preparation completed."
subject declarations subject declarations
parent-style))))) parent-style)))))
(when (and node style (not (eq node old-node))) (when (and node style (not (eq node old-node)))
(cond (let ((snapshot
((and source-stable-p (ebox-style--computed-snapshot style t)))
(ebox-style--computed-projection-transferable-p (ebox-surface--project-font
old-node node style)) node style
(ebox-style--transfer-computed-projection (and parent-id
old-node node style)) (plist-get (gethash parent-id new-nodes)
((and (null (plist-get node :ebox-computed-style)) :ebox-font-fact))
(ebox-surface--closed-canonical-node-p snapshot)
new-source-index node)) (cond
(ebox-style-apply-closed-inheritance node style)) ((and source-stable-p
(t (ebox-style--computed-projection-transferable-p
(ebox-style-apply-computed node style))) old-node node style))
(ebox-style--transfer-computed-projection
old-node node style))
((and (null (plist-get node :ebox-computed-style))
(ebox-surface--closed-canonical-node-p
new-source-index node))
(ebox-style-apply-closed-inheritance
node style snapshot))
(t
(ebox-style-apply-computed node style snapshot))))
(plist-put node :ebox-computed-style style) (plist-put node :ebox-computed-style style)
(plist-put node :ebox-candidate-computed-style-p t)) (plist-put node :ebox-candidate-computed-style-p t))
(remhash node-id visiting) (remhash node-id visiting)
@ -2303,18 +2356,35 @@ source-handle-to-subject table when the requested local preparation completed."
(when-let* ((subject (when-let* ((subject
(ebox-source--subject-table-get (ebox-source--subject-table-get
subject-table (ebox-tree-node-source-handle node)))) subject-table (ebox-tree-node-source-handle node))))
(if (and (hash-table-p ebox-surface--inline-style-value-cache) (let* ((parent-subject (ecss-subject-parent subject))
(not (ebox-surface--node-style-binding-required-p (parent-state (and parent-subject
source-index node))) (gethash parent-subject states-by-subject)))
(parent-node (and parent-state (plist-get parent-state :node)))
(parent-font-fact
(and parent-node (plist-get parent-node :ebox-font-fact)))
(precomputed-p
(plist-get node :ebox-candidate-computed-style-p))
(precomputed-style
(and precomputed-p (plist-get node :ebox-computed-style))))
(if (and (hash-table-p ebox-surface--inline-style-value-cache)
(not (ebox-surface--node-style-binding-required-p
source-index node)))
(when-let* ((parent (ecss-subject-parent subject)) (when-let* ((parent (ecss-subject-parent subject))
(binding (gethash parent bindings-by-subject))) (binding (gethash parent bindings-by-subject)))
(puthash subject binding bindings-by-subject) (puthash subject binding bindings-by-subject)
(when-let* ((state (gethash parent states-by-subject))) (when-let* ((state (gethash parent states-by-subject)))
(puthash subject state states-by-subject)) (puthash subject state states-by-subject))
(let ((style (tp-binding-read binding))) (let ((style (tp-binding-read binding)))
(unless (equal style (plist-get node :ebox-computed-style)) (unless (and precomputed-p (equal style precomputed-style))
(ebox-style-apply-closed-inheritance node style)))) (let ((snapshot (ebox-style--computed-snapshot style t)))
(let ((style (ebox-surface--project-font
node style parent-font-fact snapshot)
(unless (equal style (plist-get node :ebox-computed-style))
(ebox-style-apply-closed-inheritance
node style snapshot)))))
(when precomputed-p
(cl-remf node :ebox-candidate-computed-style-p)))
(let* ((style
(tp-binding-read (tp-binding-read
(ebox-surface--node-style-binding (ebox-surface--node-style-binding
source-index object node subject source-index object node subject
@ -2322,12 +2392,15 @@ source-handle-to-subject table when the requested local preparation completed."
style-state-table style-state-table
stylesheet-signature selector-tree-token stylesheet-signature selector-tree-token
stylesheet-subject-local-p)))) stylesheet-subject-local-p))))
(unless (equal style (plist-get node :ebox-computed-style)) (unless (and precomputed-p (equal style precomputed-style))
(if (and (null (plist-get node :ebox-computed-style)) (let ((snapshot (ebox-style--computed-snapshot style t)))
(ebox-surface--closed-canonical-node-p (ebox-surface--project-font node style parent-font-fact snapshot)
source-index node)) (unless (equal style (plist-get node :ebox-computed-style))
(ebox-style-apply-closed-inheritance node style) (if (and (null (plist-get node :ebox-computed-style))
(ebox-style-apply-computed node style))))))) (ebox-surface--closed-canonical-node-p
source-index node))
(ebox-style-apply-closed-inheritance node style snapshot)
(ebox-style-apply-computed node style snapshot))))))))))
(defun ebox-surface--static-style-state-p (state &optional seen) (defun ebox-surface--static-style-state-p (state &optional seen)
"Return non-nil when STATE's inherited binding closure is static. "Return non-nil when STATE's inherited binding closure is static.
@ -3611,8 +3684,8 @@ box with wrapper chrome falls back to the retained wrapper renderer."
(= (or (ebox-get cached-box :margin-bottom-height) 0) 0) (= (or (ebox-get cached-box :margin-bottom-height) 0) 0)
(= (or (ebox-get cached-box :border-left-pixel) 0) 0) (= (or (ebox-get cached-box :border-left-pixel) 0) 0)
(= (or (ebox-get cached-box :border-right-pixel) 0) 0) (= (or (ebox-get cached-box :border-right-pixel) 0) 0)
(not (ebox-get cached-box :border-top-p)) (= (or (ebox-get cached-box :border-top-pixel) 0) 0)
(not (ebox-get cached-box :border-bottom-p)) (= (or (ebox-get cached-box :border-bottom-pixel) 0) 0)
(eq (ebox-get cached-box :vertical-align) 'top) (eq (ebox-get cached-box :vertical-align) 'top)
(null (ebox-get cached-box :surface-properties)))) (null (ebox-get cached-box :surface-properties))))
(visible (visible
@ -4459,6 +4532,9 @@ NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges."
(when (ebox-style--text-paint-owner-p box) (when (ebox-style--text-paint-owner-p box)
(when-let* ((font (ebox-buffer--font-face box))) (when-let* ((font (ebox-buffer--font-face box)))
(push font faces)) (push font faces))
(when-let* ((decoration
(ebox-buffer--text-decoration-face box)))
(push decoration faces))
(when-let* ((color (plist-get box :color))) (when-let* ((color (plist-get box :color)))
(push (ebox-surface--foreground-face color) faces))) (push (ebox-surface--foreground-face color) faces)))
(when-let* ((background (plist-get box :bgcolor))) (when-let* ((background (plist-get box :bgcolor)))
@ -4482,12 +4558,14 @@ ANCESTOR-P means BOX wraps a more specific rendered surface."
((or ancestor-p ((or ancestor-p
(cl-intersection roles '(content content-owner pt pb pl pr))) (cl-intersection roles '(content content-owner pt pb pl pr)))
(setq faces (ebox-surface--box-face-contributions box)))) (setq faces (ebox-surface--box-face-contributions box))))
(when (and (memq 'bt roles) (plist-get box :border-top-p)) (when (and (memq 'bt roles)
(> (or (plist-get box :border-top-pixel) 0) 0))
(setq faces (setq faces
(append faces (append faces
(list (list :overline (list (list :overline
(or (plist-get box :border-top-color) t)))))) (or (plist-get box :border-top-color) t))))))
(when (and (memq 'bb roles) (plist-get box :border-bottom-p)) (when (and (memq 'bb roles)
(> (or (plist-get box :border-bottom-pixel) 0) 0))
(setq faces (setq faces
(append (append
faces faces
@ -4698,10 +4776,15 @@ relative face contributions and performs the final merge during prepare."
(when-let* ((node (gethash (plist-get dirty :node-id) nodes)) (when-let* ((node (gethash (plist-get dirty :node-id) nodes))
(box (ebox-fragment-style-source-node node)) (box (ebox-fragment-style-source-node node))
(region-id (plist-get box :region-id))) (region-id (plist-get box :region-id)))
(dolist (entry '((:border-top-p . bt) (:border-bottom-p . bb))) (dolist (entry '(((:border-top-width :border-top-style)
(when (memq (car entry) (plist-get dirty :changed-keys)) bt :border-top-pixel)
((:border-bottom-width :border-bottom-style)
bb :border-bottom-pixel)))
(when (cl-intersection (car entry)
(plist-get dirty :changed-keys))
(ebox-surface--set-horizontal-border-role (ebox-surface--set-horizontal-border-role
fragments region-id (cdr entry) (plist-get box (car entry))))))))) fragments region-id (cadr entry)
(> (or (plist-get box (caddr entry)) 0) 0))))))))
(defun ebox-surface--repaint-fragment (defun ebox-surface--repaint-fragment
(fragment old-state new-state &optional old-face-cache new-face-cache) (fragment old-state new-state &optional old-face-cache new-face-cache)
@ -4978,6 +5061,27 @@ published runtime and must be consumed without clearing those shared nodes."
author-style-pending-p author-style-pending-p
(plist-get previous-state :cascade-active-p) (plist-get previous-state :cascade-active-p)
(plist-get previous-state :cascade-required-p)))) (plist-get previous-state :cascade-required-p))))
(surface-display-signature
(and signals
(tp-signal-read
(ebox-surface--signals-display signals))))
;; Font resolution, text measurement, and final paint must see
;; one immutable host capability for the whole transaction.
(ebox--render-display-signature
(or surface-display-signature
ebox--render-display-signature
(ebox--display-signature)))
(ebox-surface--font-window
(or ebox-surface--font-window
(ebox-surface--buffer-display-window
(current-buffer))))
(_font-capability-projection
(when (and previous-state surface-display-signature
(not (equal
surface-display-signature
(plist-get previous-state
:display-signature))))
(ebox-surface--refresh-font-projections root)))
(projection (projection
(ebox-surface--projection-start (ebox-surface--projection-start
context root style-required-p projection-kind previous-state context root style-required-p projection-kind previous-state
@ -5043,10 +5147,6 @@ published runtime and must be consumed without clearing those shared nodes."
(funcall (funcall
(if (cdr axes) #'tp-signal-read #'tp-signal-peek) (if (cdr axes) #'tp-signal-read #'tp-signal-peek)
(ebox-surface--signals-viewport-height signals)))) (ebox-surface--signals-viewport-height signals))))
(display-signature
(and signals
(tp-signal-read
(ebox-surface--signals-display signals))))
(scroll-offsets (scroll-offsets
(and signals (and signals
(tp-signal-peek (tp-signal-peek
@ -5055,7 +5155,7 @@ published runtime and must be consumed without clearing those shared nodes."
(ebox-surface--projection-state (ebox-surface--projection-state
root previous-state state-overrides projection-kind root previous-state state-overrides projection-kind
stylesheet-active-p cascade-required-p axes viewport-width stylesheet-active-p cascade-required-p axes viewport-width
viewport-height display-signature scroll-offsets))) viewport-height surface-display-signature scroll-offsets)))
(plist-put state :source-index source-index) (plist-put state :source-index source-index)
(when reuse-context-axes-p (when reuse-context-axes-p
(plist-put state :viewport-dependent-node-ids (plist-put state :viewport-dependent-node-ids
@ -5140,7 +5240,8 @@ published runtime and must be consumed without clearing those shared nodes."
(ebox-surface--projection-state (ebox-surface--projection-state
root previous-state state-overrides nil root previous-state state-overrides nil
stylesheet-active-p cascade-required-p axes stylesheet-active-p cascade-required-p axes
viewport-width viewport-height display-signature viewport-width viewport-height
surface-display-signature
scroll-offsets)) scroll-offsets))
(plist-put state :source-index source-index) (plist-put state :source-index source-index)
(plist-put state :style-binding-states (plist-put state :style-binding-states

View File

@ -13,6 +13,7 @@
(require 'ecss-selector) (require 'ecss-selector)
(require 'ebox-source) (require 'ebox-source)
(require 'ebox-style) (require 'ebox-style)
(require 'ebox-node-factory)
(require 'ebox-child-range) (require 'ebox-child-range)
(defvar ebox-tree--incoming-source-indexes nil (defvar ebox-tree--incoming-source-indexes nil
@ -657,7 +658,7 @@ handles, reused node objects, and cycles without consulting node metadata."
"Properties accepted only on a direct Flex child Box.") "Properties accepted only on a direct Flex child Box.")
(defconst ebox-tree--grid-participation-keys (defconst ebox-tree--grid-participation-keys
'(:grid-column :grid-row :grid-column-span :grid-row-span :justify-self) '(:grid-column :grid-row :justify-self)
"Properties accepted only on a direct Grid child Box.") "Properties accepted only on a direct Grid child Box.")
(defconst ebox-tree--participation-keys (defconst ebox-tree--participation-keys
@ -797,10 +798,12 @@ and all explicit identities are compared with `equal'. Return ROOT on success."
(defconst ebox-tree--excluded-source-keys (defconst ebox-tree--excluded-source-keys
(append ebox-tree--runtime-source-keys (append ebox-tree--runtime-source-keys
ebox-tree--child-source-keys ebox-tree--child-source-keys
ebox-node-factory--style-projection-keys
'(:ebox-style-overrides '(:ebox-style-overrides
:ebox-source-handle :ebox-source-handle
:ebox-grid-config :ebox-grid-config
:ebox-computed-style :ebox-style-wrapper :ebox-computed-style :ebox-style-wrapper
:ebox-font-fact
:ebox-style-generated-wrapper :ebox-style-generated-wrapper
:ebox-author-style-count :ebox-author-style-required-p :ebox-author-style-count :ebox-author-style-required-p
:ebox-text-value :ebox-candidate-computed-style-p)) :ebox-text-value :ebox-candidate-computed-style-p))
@ -831,7 +834,7 @@ look like a geometry mutation."
;; retained node may arrive once from an already-expanded style ;; retained node may arrive once from an already-expanded style
;; snapshot and later from fresh shorthand declarations; comparing ;; snapshot and later from fresh shorthand declarations; comparing
;; those source spellings directly creates fake geometry dirtiness. ;; those source spellings directly creates fake geometry dirtiness.
(raw (ebox-style-expand-ebox-plist raw)) (raw (ebox-style--expand-engine-plist raw))
(table (make-hash-table :test #'eq))) (table (make-hash-table :test #'eq)))
(cl-loop for (key value) on raw by #'cddr (cl-loop for (key value) on raw by #'cddr
unless (memq key '(:color :background-color :bgcolor :host-ref unless (memq key '(:color :background-color :bgcolor :host-ref
@ -840,19 +843,9 @@ look like a geometry mutation."
(when (plist-member raw :gap) (when (plist-member raw :gap)
(let* ((gap (plist-get raw :gap)) (let* ((gap (plist-get raw :gap))
(row (if (consp gap) (car gap) gap)) (row (if (consp gap) (car gap) gap))
(column (if (and (consp gap) (cdr gap)) (cadr gap) row)) (column (if (and (consp gap) (cdr gap)) (cadr gap) row)))
;; Flex and Grid deliberately have separate canonical (puthash :row-gap row table)
;; property vocabularies. Using Grid aliases for a Flex node (puthash :column-gap column table)
;; makes an unchanged `gap' look like geometry dirtiness on
;; the next retained generation.
(row-key (if (eq (plist-get node :ebox-type) 'grid)
:grid-row-gap
:row-gap))
(column-key (if (eq (plist-get node :ebox-type) 'grid)
:grid-column-gap
:column-gap)))
(puthash row-key row table)
(puthash column-key column table)
(remhash :gap table))) (remhash :gap table)))
(let (keys) (let (keys)
(maphash (lambda (key _value) (push key keys)) table) (maphash (lambda (key _value) (push key keys)) table)

176
ebox.el
View File

@ -23,7 +23,8 @@
"Directory containing the active Ebox Lisp sources.") "Directory containing the active Ebox Lisp sources.")
(defconst ebox--compile-sources (defconst ebox--compile-sources
'("ebox-cache.el" "ebox-source.el" "ebox-style.el" "ebox-layout-config.el" '("ebox-cache.el" "ebox-source.el" "ebox-font.el" "ebox-style.el"
"ebox-layout-config.el"
"ebox-node-factory.el" "ebox-node-factory.el"
"ebox-child-range.el" "ebox-tree.el" "ebox-measure.el" "ebox-child-range.el" "ebox-tree.el" "ebox-measure.el"
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el" "ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
@ -42,6 +43,7 @@
(require 'subr-x) (require 'subr-x)
(require 'ebox-cache) (require 'ebox-cache)
(require 'ebox-source) (require 'ebox-source)
(require 'ebox-font)
(require 'ebox-style) (require 'ebox-style)
(require 'ebox-layout-config) (require 'ebox-layout-config)
(require 'ebox-node-factory) (require 'ebox-node-factory)
@ -741,78 +743,6 @@ indexes. This prepares component identity without exposing a component API."
"Compile an Ebox author form into one opaque canonical input." "Compile an Ebox author form into one opaque canonical input."
(ebox-dsl-build-input dsl)) (ebox-dsl-build-input dsl))
;;; ============================================================
;;; Property Rules: 配置驱动的 Shorthand/Longhand 映射
;;; ============================================================
;;
;; 规则格式自描述,无需 type 标记:
;; - 值是 list → shorthand展开到列表中的属性
;; - 值是 (keyword . converter) → longhand转换后写入目标
;; - 值是 keyword → 直通,属性名不变
(defconst ebox-property-rules ebox-style-ebox-property-rules
"Compatibility alias for Ebox public property expansion rules.")
;;; --- 值转换器 ---
(defun ebox--convert (value converter)
"Convert VALUE using CONVERTER."
(ebox-style--convert value converter))
;;; --- 值拆分器 ---
(defun ebox--split-trbl (value)
"Split VALUE using CSS TRBL rules → (top right bottom left)."
(ebox-style--split-trbl value))
(defun ebox--split-pair (value)
"Split VALUE into pair → (start end)."
(ebox-style--split-pair value))
(defconst ebox--border-styles
'(none hidden dotted dashed solid double groove ridge inset outset)
"Valid CSS border-style keywords.")
(defun ebox--wsc-classify (item)
"Classify ITEM as width, style, or color. Returns (type . value)."
(ebox-style--wsc-classify item))
(defun ebox--split-wsc (value)
"Split VALUE into (width style color) using CSS type-based parsing.
Values can appear in any order; each is identified by type."
(ebox-style--split-wsc value))
(defun ebox--split-value (value mode n)
"Split VALUE according to MODE for N targets."
(ebox-style--split-value value mode n))
;;; --- 核心展开函数 ---
(defun ebox--get-rule (property)
"Get rule for PROPERTY from `ebox-property-rules'."
(ebox-style--get-ebox-rule property))
(defun ebox--expand-property (property value)
"Expand PROPERTY with VALUE according to rules. Returns a plist."
(ebox-style-expand-ebox-property property value))
;;; --- 辅助函数 ---
(defun ebox--atom-consp (list)
"Check if LIST is a strict dotted pair (atom . atom)."
(ebox-style--atom-consp list))
(defun ebox--parse-color (color)
"Parse COLOR to a real color string.
Supports: t (default fg), nil, string, (light . dark) cons."
(ebox-style--parse-color color))
(defun ebox--expand-plist (plist)
"Expand PLIST using `ebox-property-rules'."
(ebox-style-expand-ebox-plist plist))
;;; --- 核心 API ---
;;; ============================================================ ;;; ============================================================
;;; Buffer Runtime State & Scroll State ;;; Buffer Runtime State & Scroll State
;;; ============================================================ ;;; ============================================================
@ -1308,8 +1238,8 @@ current visible slice. Prefix misses continue through the normal renderer."
(= (or (ebox-get box :margin-bottom-height) 0) 0) (= (or (ebox-get box :margin-bottom-height) 0) 0)
(= (or (ebox-get box :border-left-pixel) 0) 0) (= (or (ebox-get box :border-left-pixel) 0) 0)
(= (or (ebox-get box :border-right-pixel) 0) 0) (= (or (ebox-get box :border-right-pixel) 0) 0)
(not (ebox-get box :border-top-p)) (= (or (ebox-get box :border-top-pixel) 0) 0)
(not (ebox-get box :border-bottom-p)) (= (or (ebox-get box :border-bottom-pixel) 0) 0)
(eq (ebox-get box :vertical-align) 'top) (eq (ebox-get box :vertical-align) 'top)
(null (ebox-get box :surface-properties)) (null (ebox-get box :surface-properties))
visible))) visible)))
@ -1360,8 +1290,8 @@ proof becomes usable for an input event."
(= (or (ebox-get box :margin-bottom-height) 0) 0) (= (or (ebox-get box :margin-bottom-height) 0) 0)
(= (or (ebox-get box :border-left-pixel) 0) 0) (= (or (ebox-get box :border-left-pixel) 0) 0)
(= (or (ebox-get box :border-right-pixel) 0) 0) (= (or (ebox-get box :border-right-pixel) 0) 0)
(not (ebox-get box :border-top-p)) (= (or (ebox-get box :border-top-pixel) 0) 0)
(not (ebox-get box :border-bottom-p)) (= (or (ebox-get box :border-bottom-pixel) 0) 0)
(eq (ebox-get box :vertical-align) 'top) (eq (ebox-get box :vertical-align) 'top)
(null (ebox-get box :surface-properties))))) (null (ebox-get box :surface-properties)))))
@ -3831,23 +3761,44 @@ Defaults to the current buffer."
(nreverse result))) (nreverse result)))
(defconst ebox--region-update-longhand-props (defconst ebox--region-update-longhand-props
'(:box-sizing (append
'(:box-sizing
:width :min-width :max-width :width :min-width :max-width
:height :min-height :max-height :height :min-height :max-height
:padding-left-pixel :padding-right-pixel :padding-left-pixel :padding-right-pixel
:padding-top-height :padding-bottom-height :padding-top-height :padding-bottom-height
:margin-left-pixel :margin-right-pixel :margin-left-pixel :margin-right-pixel
:margin-top-height :margin-bottom-height :margin-top-height :margin-bottom-height
:border-left-pixel :border-left-color :border-left-pixel :border-left-style :border-left-color
:border-right-pixel :border-right-color :border-right-pixel :border-right-style :border-right-color
:border-top-p :border-top-color :border-top-pixel :border-top-style :border-top-color
:border-bottom-p :border-bottom-color :border-bottom-pixel :border-bottom-style :border-bottom-color
:color :bgcolor :color :bgcolor
:text-align :vertical-align :text-align :vertical-align
:overflow :wrap-mode :overflow :wrap-mode
:scroll-offset :visibility) :scroll-offset :visibility)
ebox-style--item-projection-properties)
"Longhand box properties accepted by `ebox-region-update'.") "Longhand box properties accepted by `ebox-region-update'.")
(defconst ebox--region-update-control-properties
'(:content :scroll-offset)
"Surface controls accepted by `ebox-region-update' outside style schemas.")
(defun ebox--region-update-style-properties (props)
"Return PROPS without region-owned content and scroll controls."
(cl-loop for (property value) on props by #'cddr
unless (memq property ebox--region-update-control-properties)
append (list property value)))
(defun ebox--region-update-author-form (box)
"Return the public author form whose closed schema applies to BOX."
(if (eq (plist-get box :ebox-kind) 'text)
'text
(pcase (ebox-tree-layout-kind box)
('normal 'box)
((and kind (or 'row 'column 'flex 'grid)) kind)
(_ 'box))))
(defun ebox--region-update-normalize-property (property) (defun ebox--region-update-normalize-property (property)
"Return the box longhand field for region-update PROPERTY." "Return the box longhand field for region-update PROPERTY."
(pcase property (pcase property
@ -3884,6 +3835,8 @@ Defaults to the current buffer."
((memq target-key ebox--region-update-longhand-props) ((memq target-key ebox--region-update-longhand-props)
(when (not (equal (ebox-get box target-key) value)) (when (not (equal (ebox-get box target-key) value))
(setq style-changed-p t))) (setq style-changed-p t)))
((ebox-style-schema-id key)
(setq style-changed-p t))
(t (t
(push key unknown-keys)))) (push key unknown-keys))))
(when (and content-seen (not (stringp content-value))) (when (and content-seen (not (stringp content-value)))
@ -3928,8 +3881,35 @@ cannot be changed through the unpublished batch root."
buffer region-id t)) buffer region-id t))
(ebox-tree-copy-node-structure (plist-get state :root-node))))) (ebox-tree-copy-node-structure (plist-get state :root-node)))))
(defun ebox--region-update-layout-delta (box declarations)
"Apply LayoutConfig DECLARATIONS to BOX and return changed property names."
(when-let* ((config (and (ebox-box-node-p box)
(ebox-box-node-layout box)))
(kind (ebox-layout-config-kind config))
(author-kind (if (eq kind 'normal) 'box kind))
(names (ebox-layout-config-property-names author-kind))
(delta
(ebox-style-declaration-properties
declarations
(lambda (property)
(memq (plist-get property :name) names)))))
(let* ((normalized
(ebox-layout-config-props
(ebox-layout-config-for-form author-kind delta)))
(props (copy-tree (ebox-layout-config-props config)))
changed)
(cl-loop for (name _value) on delta by #'cddr
for value = (plist-get normalized name)
unless (equal (plist-get props name) value)
do (setq props (plist-put props name value))
and do (push name changed))
(when changed
(plist-put box :ebox-layout-config
(ebox-layout-config-with-props config props)))
(nreverse changed))))
(defun ebox--surface-region-apply-props (defun ebox--surface-region-apply-props
(root source-index region-id expanded declarations) (root source-index region-id expanded declarations style-declarations)
"Apply DECLARATIONS then EXPANDED fields to REGION-ID in ROOT." "Apply DECLARATIONS then EXPANDED fields to REGION-ID in ROOT."
(let ((box (ebox--root-region-box root region-id)) (let ((box (ebox--root-region-box root region-id))
(candidate-source-index source-index) (candidate-source-index source-index)
@ -3947,6 +3927,17 @@ cannot be changed through the unpublished batch root."
source-index old-handle :declarations declarations))) source-index old-handle :declarations declarations)))
(setq candidate-source-index (car binding)) (setq candidate-source-index (car binding))
(plist-put box :ebox-source-handle (cdr binding)))) (plist-put box :ebox-source-handle (cdr binding))))
(when (plist-member style-declarations 'ebox/outer)
(let* ((outer (plist-get style-declarations 'ebox/outer))
(display (plist-get box :display))
(next (list outer (or (cadr display) 'flow))))
(unless (equal display next)
(plist-put box :display next)
(push :outer changed-keys))))
(setq changed-keys
(nconc (ebox--region-update-layout-delta
box style-declarations)
changed-keys))
(cl-loop for (key value) on expanded by #'cddr (cl-loop for (key value) on expanded by #'cddr
for target-key = (ebox--region-update-normalize-property key) for target-key = (ebox--region-update-normalize-property key)
when (or (eq target-key :content) when (or (eq target-key :content)
@ -4195,12 +4186,23 @@ REGION-ID and CHANGED-KEYS describe one update; CHANGES describes a batch."
(candidate-box (candidate-box
(or (ebox--root-region-box published-root region-id) (or (ebox--root-region-box published-root region-id)
(user-error "Ebox region handle no longer resolves to a box"))) (user-error "Ebox region handle no longer resolves to a box")))
(expanded (ebox--expand-plist props)) (style-declarations
(ebox-style-compile-form
(ebox--region-update-author-form candidate-box)
(ebox--region-update-style-properties props)))
(declarations (declarations
(ebox-style-merge-declarations (ebox-style-merge-declarations
(ebox-style-node-declarations (ebox-style-node-declarations
source-index candidate-box) source-index candidate-box)
(ebox-style-compile-declarations props))) style-declarations))
(expanded
(append
(cl-loop for (property value) on props by #'cddr
when (memq property ebox--region-update-control-properties)
append (list property value))
(ebox-style--expand-engine-delta
style-declarations declarations
(plist-get candidate-box :ebox-computed-style))))
(old-style-values (old-style-values
(and (plist-member expanded :overflow) (and (plist-member expanded :overflow)
(list :overflow (ebox-get candidate-box :overflow)))) (list :overflow (ebox-get candidate-box :overflow))))
@ -4235,7 +4237,7 @@ REGION-ID and CHANGED-KEYS describe one update; CHANGES describes a batch."
(applied (applied
(ebox--surface-region-apply-props (ebox--surface-region-apply-props
candidate-root source-index candidate-root source-index
region-id expanded declarations)) region-id expanded declarations style-declarations))
(changed-keys (plist-get applied :changed-keys)) (changed-keys (plist-get applied :changed-keys))
(candidate-source-index (plist-get applied :source-index)) (candidate-source-index (plist-get applied :source-index))
(dirty-kind (dirty-kind

View File

@ -644,10 +644,12 @@
(cl-labels (cl-labels
((rows (selected) ((rows (selected)
(cl-loop for id from 1 to 12 (cl-loop for id from 1 to 12
for content = (ebox-test-text (format "row-%02d" id))
collect collect
(ebox-test-box (if (= id selected)
:key id (ebox-test-text (format "row-%02d" id)) :width '(420) (ebox-test-box
:color (and (= id selected) "#2F6B43")))) :key id :width '(420) :color "#2F6B43" content)
(ebox-test-box :key id :width '(420) content))))
(root () (root ()
(ebox-test-box (ebox-test-box
:width '(900) :color "#172033" :wrap-mode 'none :width '(900) :color "#172033" :wrap-mode 'none
@ -976,7 +978,7 @@
(let* ((buffer (generate-new-buffer " *ebox-range-no-flatten*")) (let* ((buffer (generate-new-buffer " *ebox-range-no-flatten*"))
(static (static
(cl-loop for index below 100 (cl-loop for index below 100
collect (ebox-test-box :key (list 'static index) collect (ebox-test-box :key (format "static-%d" index)
(ebox-test-text (number-to-string index))))) (ebox-test-text (number-to-string index)))))
(root (apply #'ebox-test-column (root (apply #'ebox-test-column
(append static (append static
@ -1042,7 +1044,7 @@
(apply #'ebox-test-column (apply #'ebox-test-column
(append (append
(cl-loop for index below static-count (cl-loop for index below static-count
collect (ebox-test-box :key (list 'static index) collect (ebox-test-box :key (format "static-%d" index)
(ebox-test-text "s"))) (ebox-test-text "s")))
(list (ebox-test-child-range (list (ebox-test-child-range
'items (ebox-test-box :key 'old (ebox-test-text "o")))))))) 'items (ebox-test-box :key 'old (ebox-test-text "o"))))))))
@ -1238,7 +1240,7 @@
(apply #'ebox-test-column (apply #'ebox-test-column
(append (append
(cl-loop for index below 500 (cl-loop for index below 500
collect (ebox-test-box :key (list 'static index) collect (ebox-test-box :key (format "static-%d" index)
(ebox-test-text "s"))) (ebox-test-text "s")))
(list (ebox-test-child-range 'items))))) (list (ebox-test-child-range 'items)))))
report) report)
@ -1250,7 +1252,7 @@
candidate 'items candidate 'items
(ebox-child-range-test--input (ebox-child-range-test--input
(cl-loop for index below 64 (cl-loop for index below 64
collect (ebox-test-box :key (list 'new index) collect (ebox-test-box :key (format "new-%d" index)
(ebox-test-text "n"))))) (ebox-test-text "n")))))
(cl-letf (((symbol-function (cl-letf (((symbol-function
'ebox-incremental--candidate-full-index-preparation) 'ebox-incremental--candidate-full-index-preparation)

View File

@ -595,8 +595,8 @@
"Repeated equivalent style declarations compile through ECSS once." "Repeated equivalent style declarations compile through ECSS once."
(clrhash ebox-style--declaration-cache) (clrhash ebox-style--declaration-cache)
(let ((calls 0) (let ((calls 0)
(original (symbol-function 'ecss-merge-declarations))) (original (symbol-function 'ecss-expand-declarations)))
(cl-letf (((symbol-function 'ecss-merge-declarations) (cl-letf (((symbol-function 'ecss-expand-declarations)
(lambda (&rest arguments) (lambda (&rest arguments)
(cl-incf calls) (cl-incf calls)
(apply original arguments)))) (apply original arguments))))
@ -1038,57 +1038,6 @@
(kill-buffer buffer))))) (kill-buffer buffer)))))
(ert-deftest ebox-commit-keeps-root-owner-for-dynamic-cascade-source ()
"A TP-dependent cascade binding must preserve root-owner fallback."
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
(color (tp-signal-create "#111111"))
(calls 0)
(buffer nil))
(ebox-style-add-rule
".card" (list :color (tp-computed (lambda () (tp-signal-read color))))
:layer 'components)
(unwind-protect
(progn
(setq buffer
(ebox-render-to-buffer
(generate-new-buffer-name " *ebox-dynamic-style-proof*")
(ebox-test-column
(ebox-test-box :key 'card :class "card" (ebox-test-text "Before")
:width '(40))
(ebox-test-box :key 'stable (ebox-test-text "Stable")
:width '(40)))))
(let* ((state (ebox--buffer-render-state buffer))
(style-states (plist-get state :style-binding-states))
(binding-state
(cl-loop for value being the hash-values of style-states
when (and (plist-get value :binding)
(not (ebox-surface--static-style-state-p
value)))
return value)))
(should binding-state)
(should (> (tp-binding-dependency-count
(plist-get binding-state :binding))
(if (plist-get binding-state :parent-binding) 1 0)))
(should-not (ebox-surface--static-style-state-p binding-state)))
(let ((original
(symbol-function 'ebox-incremental--layout-owner-plan)))
(cl-letf (((symbol-function 'ebox-incremental--layout-owner-plan)
(lambda (&rest arguments)
(cl-incf calls)
(apply original arguments))))
(ebox-commit
buffer
(ebox-test-column
(ebox-test-box :key 'card :class "card" (ebox-test-text "After")
:width '(40))
(ebox-test-box :key 'stable (ebox-test-text "Stable")
:width '(40))))
(should (= calls 0)))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-commit-formatting-context-reflow-owns-variable-line-siblings () (ert-deftest ebox-commit-formatting-context-reflow-owns-variable-line-siblings ()
"Two variable-line owners should publish through their nearest stack context. "Two variable-line owners should publish through their nearest stack context.
The context owns the complete local block; the root and untouched header/footer The context owns the complete local block; the root and untouched header/footer
@ -1299,7 +1248,7 @@ remain retained identities."
(ebox-test-box :key 'right :source-identity 'right (ebox-test-text "R"))) (ebox-test-box :key 'right :source-identity 'right (ebox-test-text "R")))
(old-root (old-root
(ebox-test-grid :key 'grid :width '(80) :grid-template-columns (ebox-test-grid :key 'grid :width '(80) :grid-template-columns
'((36) (36)) :column-gap '(0 (8)) '((36) (36)) :column-gap '(8)
(ebox-test-box :key 'left :source-identity 'left (ebox-test-text "left-old")) (ebox-test-box :key 'left :source-identity 'left (ebox-test-text "left-old"))
(ebox-test-box :key 'right :source-identity 'right (ebox-test-text "right-old")))) (ebox-test-box :key 'right :source-identity 'right (ebox-test-text "right-old"))))
(buffer nil) (buffer nil)
@ -1320,7 +1269,10 @@ remain retained identities."
'(span-patch owner-scoped))) '(span-patch owner-scoped)))
(should-not (plist-get report :tp-full-root)) (should-not (plist-get report :tp-full-root))
(should-not (plist-get report :tp-scope-fallback)) (should-not (plist-get report :tp-scope-fallback))
(should (= 1 (plist-get report :tp-text-operations))) ;; The 8px Grid gap keeps the two changed slots disjoint. TP still
;; publishes one scoped transaction, with one exact text operation
;; per changed slot instead of replacing the unchanged gap.
(should (= 2 (plist-get report :tp-text-operations)))
(should (string-match-p "L" (should (string-match-p "L"
(ebox-commit-test--buffer-string buffer))) (ebox-commit-test--buffer-string buffer)))
(setq fresh (setq fresh
@ -1328,7 +1280,7 @@ remain retained identities."
(generate-new-buffer-name (generate-new-buffer-name
" *ebox-multi-owner-variable-slots-fresh*") " *ebox-multi-owner-variable-slots-fresh*")
(ebox-test-grid :key 'grid :width '(80) :grid-template-columns (ebox-test-grid :key 'grid :width '(80) :grid-template-columns
'((36) (36)) :column-gap '(0 (8)) '((36) (36)) :column-gap '(8)
(ebox-test-box :key 'left :source-identity 'left (ebox-test-text "L")) (ebox-test-box :key 'left :source-identity 'left (ebox-test-text "L"))
(ebox-test-box :key 'right :source-identity 'right (ebox-test-text "R"))))) (ebox-test-box :key 'right :source-identity 'right (ebox-test-text "R")))))
(should (equal (should (equal
@ -1347,7 +1299,7 @@ remain retained identities."
((root (left right) ((root (left right)
(ebox-test-grid :key 'grid :width '(80) (ebox-test-grid :key 'grid :width '(80)
:grid-template-columns '((36) (36)) :grid-template-columns '((36) (36))
:column-gap '(0 (8)) :column-gap '(8)
(ebox-test-box :key 'left :source-identity 'left (ebox-test-text left)) (ebox-test-box :key 'left :source-identity 'left (ebox-test-text left))
(ebox-test-box :key 'right :source-identity 'right (ebox-test-text right)))) (ebox-test-box :key 'right :source-identity 'right (ebox-test-text right))))
(candidate (buffer) (candidate (buffer)
@ -1682,7 +1634,7 @@ remain retained identities."
:key 'geometry-context :width '(80) :key 'geometry-context :width '(80)
(ebox-test-column (ebox-test-column
(ebox-test-box :key 'left :source-identity 'left (ebox-test-box :key 'left :source-identity 'left
(ebox-test-text left) :font 'bold) (ebox-test-text left) :font-weight 'bold)
(ebox-test-box :key 'right :source-identity 'right (ebox-test-box :key 'right :source-identity 'right
(ebox-test-text right)))) (ebox-test-text right))))
(ebox-test-box :key 'paint-a :source-identity 'paint-a (ebox-test-box :key 'paint-a :source-identity 'paint-a
@ -1867,7 +1819,7 @@ remain retained identities."
(ebox-candidate-replace-host-ref (ebox-candidate-replace-host-ref
candidate 'left candidate 'left
(ebox-test-box :key 'left :source-identity 'left (ebox-test-box :key 'left :source-identity 'left
(ebox-test-text left) :font 'bold)) (ebox-test-text left) :font-weight 'bold))
(ebox-candidate-replace-host-ref (ebox-candidate-replace-host-ref
candidate 'right candidate 'right
(ebox-test-box :key 'right :source-identity 'right (ebox-test-box :key 'right :source-identity 'right
@ -1991,28 +1943,39 @@ remain retained identities."
(defun ebox-commit-test--allocation-closure-root (defun ebox-commit-test--allocation-closure-root
(toast paint-a paint-b &optional width footer-overflow root-overflow) (toast paint-a paint-b &optional width footer-overflow root-overflow)
"Return a generic whole-line Flex allocation-closure fixture." "Return a generic whole-line Flex allocation-closure fixture."
(ebox-test-box (let* ((footer
:key 'root :width (list (or width 800)) (apply
:height (and root-overflow 1) :overflow root-overflow #'ebox-test-flex
(ebox-test-column (append
(ebox-test-box :key 'status :source-identity 'status (list :key 'footer :width (list (or width 800))
(ebox-test-text "Theme: Light") :width '(200)) :flex-wrap 'wrap :gap '(1 (4)))
(ebox-test-box :key 'paint-a :source-identity 'paint-a (when footer-overflow (list :overflow footer-overflow))
(ebox-test-text "paint-a") :color paint-a) (list
(ebox-test-box :key 'paint-b :source-identity 'paint-b (ebox-test-box
(ebox-test-text "paint-b") :bgcolor paint-b) :key 'toast-slot :width 'stretch :min-width 0
(ebox-test-box :flex-grow 1 :flex-shrink 1 :flex-basis '(0)
:key 'footer-owner :width (list (or width 800)) (ebox-test-column
(ebox-test-column (ebox-test-box :key 'toast :source-identity 'toast
(ebox-test-flex (ebox-test-text toast))))
:key 'footer :width (list (or width 800)) :flex-wrap 'wrap (ebox-test-box :key 'peer
:overflow footer-overflow :gap '(1 (4)) (ebox-test-text "database.sqlite"))))))
(ebox-test-box (content
:key 'toast-slot :width 'stretch :min-width 0 (ebox-test-column
:flex-grow 1 :flex-shrink 1 :flex-basis '(0) (ebox-test-box :key 'status :source-identity 'status
(ebox-test-column (ebox-test-text "Theme: Light") :width '(200))
(ebox-test-box :key 'toast :source-identity 'toast (ebox-test-text toast)))) (ebox-test-box :key 'paint-a :source-identity 'paint-a
(ebox-test-box :key 'peer (ebox-test-text "database.sqlite")))))))) (ebox-test-text "paint-a") :color paint-a)
(ebox-test-box :key 'paint-b :source-identity 'paint-b
(ebox-test-text "paint-b") :bgcolor paint-b)
(ebox-test-box
:key 'footer-owner :width (list (or width 800))
(ebox-test-column footer)))))
(apply
#'ebox-test-box
(append
(list :key 'root :width (list (or width 800)))
(when root-overflow (list :height 1 :overflow root-overflow))
(list content)))))
(defun ebox-commit-test--allocation-closure-candidate (defun ebox-commit-test--allocation-closure-candidate
(buffer toast paint-a paint-b) (buffer toast paint-a paint-b)

View File

@ -380,41 +380,37 @@
full-updates)) full-updates))
(defconst ebox-test--region-update-paint-cases (defconst ebox-test--region-update-paint-cases
'((:color "#123456" "#123456") '((:color "#123456" :color "#123456")
(:bgcolor "#FDE68A" "#FDE68A") (:bgcolor "#FDE68A" :bgcolor "#FDE68A")
(:border-left-color "#111111" "#111111") (:border-left-color "#111111" :border-left-color "#111111")
(:border-right-color "#222222" "#222222") (:border-right-color "#222222" :border-right-color "#222222")
(:border-top-color "#333333" "#333333") (:border-top-color "#333333" :border-top-color "#333333")
(:border-bottom-color "#444444" "#444444") (:border-bottom-color "#444444" :border-bottom-color "#444444")
(:border-top-p t t) (:visibility hidden :visibility hidden))
(:border-bottom-p t t)) "Paint updates as (AUTHOR VALUE ENGINE-FIELD EXPECTED).")
"Paint-only box longhand update cases: (PROPERTY VALUE EXPECTED).")
(defconst ebox-test--region-update-geometry-cases (defconst ebox-test--region-update-geometry-cases
'((:box-sizing content-box content-box owner-rerender) '((:box-sizing content-box :box-sizing content-box owner-rerender)
(:width (120) 120 owner-rerender) (:width (120) :width 120 owner-rerender)
(:min-width (100) 100 owner-rerender) (:min-width (100) :min-width 100 owner-rerender)
(:max-width (140) 140 owner-rerender) (:max-width (140) :max-width 140 owner-rerender)
(:height 3 3 owner-rerender) (:height 3 :height 3 owner-rerender)
(:min-height 3 3 owner-rerender) (:min-height 3 :min-height 3 owner-rerender)
(:max-height 1 1 owner-rerender) (:max-height 1 :max-height 1 owner-rerender)
(:padding-left-pixel 9 9 span-patch) (:padding-left (9) :padding-left-pixel 9 span-patch)
(:padding-right-pixel 10 10 span-patch) (:padding-right (10) :padding-right-pixel 10 span-patch)
(:padding-top-height 1 1 span-patch) (:padding-top 1 :padding-top-height 1 span-patch)
(:padding-bottom-height 1 1 span-patch) (:padding-bottom 1 :padding-bottom-height 1 span-patch)
(:margin-left-pixel 7 7 owner-rerender) (:margin-left (7) :margin-left-pixel 7 owner-rerender)
(:margin-right-pixel 8 8 owner-rerender) (:margin-right (8) :margin-right-pixel 8 owner-rerender)
(:margin-top-height 1 1 owner-rerender) (:margin-top 1 :margin-top-height 1 owner-rerender)
(:margin-bottom-height 1 1 owner-rerender) (:margin-bottom 1 :margin-bottom-height 1 owner-rerender)
(:border-left-pixel 5 5 span-patch) (:border-top-p t :border-top-pixel 1 span-patch)
(:border-right-pixel 6 6 span-patch) (:text-align center :text-align center span-patch)
(:text-align center center span-patch) (:overflow hidden :overflow hidden owner-rerender)
(:vertical-align middle center span-patch) (:wrap-mode char :wrap-mode char span-patch)
(:overflow hidden hidden owner-rerender) (:scroll-offset 1 :scroll-offset 1 span-patch))
(:wrap-mode char char span-patch) "Geometry updates as (AUTHOR VALUE ENGINE-FIELD EXPECTED STRATEGY).")
(:scroll-offset 1 1 span-patch)
(:visibility hidden hidden span-patch))
"Geometry box longhand update cases: (PROPERTY VALUE EXPECTED STRATEGY).")
(defun ebox-test--region-update-case-properties (cases) (defun ebox-test--region-update-case-properties (cases)
"Return the update property names covered by CASES." "Return the update property names covered by CASES."
@ -463,22 +459,15 @@
(should (ebox-test--face-has-key-value-p (should (ebox-test--face-has-key-value-p
face :background "#1E293B"))))) face :background "#1E293B")))))
(ert-deftest ebox-ecss-value-resolver-uses-tp-computed-contract () (ert-deftest ebox-font-size-rejects-runtime-and-function-values ()
"ECSS should resolve tagged TP values while preserving literal functions." "Standalone Ebox font size accepts only positive CSS reference pixels."
(let ((calls 0) (should-error
(literal (lambda () 'literal))) (ebox-style-compute
(should (list :font-size (tp-computed (lambda () 16))))
(= (plist-get :type 'error)
(ebox-style-compute (should-error
(list :font-height (ebox-style-compute (list :font-size (lambda () 16)))
(tp-computed (lambda () (cl-incf calls) 1.25)))) :type 'error))
:font-height)
1.25))
(should (= calls 1))
(should
(eq (plist-get (ebox-style-compute (list :font-height literal))
:font-height)
literal))))
(ert-deftest ebox-ecss-computed-fact-retains-owner-impact-and-provenance () (ert-deftest ebox-ecss-computed-fact-retains-owner-impact-and-provenance ()
"The Ebox package schema should remain attached to one computed fact." "The Ebox package schema should remain attached to one computed fact."
@ -513,6 +502,11 @@
(paint-value (ecss-computed-property-fact-value fact))) (paint-value (ecss-computed-property-fact-value fact)))
(should (equal '(geometry paint) (should (equal '(geometry paint)
(ecss-computed-property-fact-impacts fact))) (ecss-computed-property-fact-impacts fact)))
(should (equal '(measurement paint)
(ecss-computed-property-fact-projections fact)))
(should (eq (plist-get
(ecss-computed-property-fact-metadata fact) :owner)
'font-metrics-projection))
(should (eq geometry-value paint-value)) (should (eq geometry-value paint-value))
(should (equal geometry-value (should (equal geometry-value
(ecss-computed-style-value style 'ebox/font-weight))))) (ecss-computed-style-value style 'ebox/font-weight)))))
@ -551,7 +545,9 @@
(lambda (computed) (lambda (computed)
(cl-incf provenance-reads) (cl-incf provenance-reads)
(funcall original-provenance computed)))) (funcall original-provenance computed))))
(should (ebox-style--context-values style 'box t))) (should (ebox-style--context-values style 'box t))
(should (ebox-style--context-values style 'box t))
(should (ebox-style--context-values style 'box nil)))
(should (= values-reads 1)) (should (= values-reads 1))
(should (= active-reads 1)) (should (= active-reads 1))
(should (= specified-reads 1)) (should (= specified-reads 1))
@ -590,31 +586,6 @@
(should (= active-reads 1)) (should (= active-reads 1))
(should (= specified-reads 1)))) (should (= specified-reads 1))))
(ert-deftest ebox-cascade-inherits-color-but-honors-explicit-nil ()
"A child nil color should block inheritance without inheriting background."
(ebox-test--reset-runtime-state)
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
(root (ebox-test-box :class 'panel :wrap-mode 'none :width '(96)
(ebox-test-box :id 'child :color nil :width '(96)
(ebox-test-text "Child")))))
(ebox-style-add-rule
".panel" '(:color "#E2E8F0" :bgcolor "#0F172A")
:layer 'components)
(ebox-test--with-rendered-buffer root
(let* ((runtime-root (ebox-test--runtime-root (current-buffer)))
(runtime-child (car (ebox-tree-layout-children runtime-root)))
(child-style (plist-get runtime-child :ebox-computed-style))
(values (ecss-computed-style-values child-style))
(active (ecss-computed-style-active-properties child-style)))
(should (memq 'ebox/color active))
(should (null (plist-get values 'ebox/color)))
(should (null (plist-get values 'ebox/background-color)))
(should-not (memq 'ebox/background-color active))
(goto-char (point-min))
(search-forward "Child")
(let ((face (get-text-property (match-beginning 0) 'face)))
(should (equal (car face) '(:inherit default))))))))
(ert-deftest ebox-default-foreground-reset-preserves-explicit-background () (ert-deftest ebox-default-foreground-reset-preserves-explicit-background ()
"Resolve GUI default foreground without inheriting its background." "Resolve GUI default foreground without inheriting its background."
(let ((original (symbol-function 'face-attribute))) (let ((original (symbol-function 'face-attribute)))
@ -700,8 +671,8 @@
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((root (let* ((root
(ebox-test-box :wrap-mode 'none :font-family "Parent Family" (ebox-test-box :wrap-mode 'none :font-family "Parent Family"
:font-height 1.25 :font-weight 'bold :font-size 16 :font-weight 'bold
(ebox-test-box :id 'child :font-family "Child Family" (ebox-test-box :id 'child :font-family "Child Family"
(ebox-test-text "MMMM")))) (ebox-test-text "MMMM"))))
measured-face) measured-face)
(cl-letf (((symbol-function 'ebox--string-pixel-width) (cl-letf (((symbol-function 'ebox--string-pixel-width)
@ -716,18 +687,18 @@
(values (ecss-computed-style-values style)) (values (ecss-computed-style-values style))
(face (ebox-test--first-content-face (buffer-string)))) (face (ebox-test--first-content-face (buffer-string))))
(should (equal (plist-get values 'ebox/font-family) "Child Family")) (should (equal (plist-get values 'ebox/font-family) "Child Family"))
(should (= (plist-get values 'ebox/font-height) 1.25)) (should (= (plist-get values 'ebox/font-size) 16))
(should (eq (plist-get values 'ebox/font-weight) 'bold)) (should (eq (plist-get values 'ebox/font-weight) 'bold))
(should (should
(equal (ebox-test--effective-face-value face :family) (equal (ebox-test--effective-face-value face :family)
"Child Family")) "Child Family"))
(should (= (ebox-test--effective-face-value face :height) 1.25)) (should (= (ebox-test--effective-face-value face :height) 120))
(should (eq (ebox-test--effective-face-value face :weight) 'bold))))) (should (eq (ebox-test--effective-face-value face :weight) 'bold)))))
(should measured-face) (should measured-face)
(should (should
(equal (ebox-test--effective-face-value measured-face :family) (equal (ebox-test--effective-face-value measured-face :family)
"Child Family")) "Child Family"))
(should (= (ebox-test--effective-face-value measured-face :height) 1.25)))) (should (= (ebox-test--effective-face-value measured-face :height) 120))))
(ert-deftest ebox-column-background-owns-blank-lines-with-child-override () (ert-deftest ebox-column-background-owns-blank-lines-with-child-override ()
"A column background should fill its blank lines without hiding child paint." "A column background should fill its blank lines without hiding child paint."
@ -1781,6 +1752,31 @@
(should props) (should props)
(should (plist-member props 'face)))) (should (plist-member props 'face))))
(ert-deftest ebox-text-decoration-projects-to-one-emacs-face ()
"Canonical decoration facts should remain paint-only and visibly compose."
(let* ((style
(ebox-style-compute
'(:text-decoration-line (underline overline line-through)
:text-decoration-color "#336699"
:text-decoration-style wavy)))
(face (plist-get
(ebox-buffer-paint-text-properties style 'content) 'face))
(rendered
(ebox-render
(ebox-test-text
"Decorated"
:text-decoration-line '(underline overline line-through)
:text-decoration-color "#336699"
:text-decoration-style 'wavy)))
(rendered-face (get-text-property 0 'face rendered)))
(dolist (candidate (list face rendered-face))
(should (ebox-test--face-has-key-value-p
candidate :underline '(:color "#336699" :style wave)))
(should (ebox-test--face-has-key-value-p
candidate :overline "#336699"))
(should (ebox-test--face-has-key-value-p
candidate :strike-through "#336699")))))
(ert-deftest ebox-render-keeps-tp-paint-slot-address-stable () (ert-deftest ebox-render-keeps-tp-paint-slot-address-stable ()
"A TP paint slot should recolor retained output without property rewrites." "A TP paint slot should recolor retained output without property rewrites."
(let* ((slot (tp-paint-slot-create '(:foreground "red"))) (let* ((slot (tp-paint-slot-create '(:foreground "red")))
@ -2120,7 +2116,7 @@
"Detailed snapshots should expose explicit L2 footprint signatures." "Detailed snapshots should expose explicit L2 footprint signatures."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((box (ebox-test-box (ebox-test-text "Footprint") :width '(120) (let* ((box (ebox-test-box (ebox-test-text "Footprint") :width '(120)
:height 1 :border t)) :height 1 :border '(1 solid)))
(node-id (ebox--ensure-node-id (ebox-test-root box)))) (node-id (ebox--ensure-node-id (ebox-test-root box))))
(ebox-test--with-rendered-buffer box (ebox-test--with-rendered-buffer box
(let* ((snapshot (ebox--ensure-layout-snapshot-details (let* ((snapshot (ebox--ensure-layout-snapshot-details
@ -2531,7 +2527,7 @@
collect collect
(ebox-test-box (ebox-test-box
(ebox-test-text (format "Fluid %02d" index)) (ebox-test-text (format "Fluid %02d" index))
:border t)))) :border '(1 solid)))))
buffer) buffer)
(unwind-protect (unwind-protect
(progn (progn
@ -2571,7 +2567,7 @@
collect collect
(ebox-test-box (ebox-test-box
(ebox-test-text (format "Fluid section %02d" index)) (ebox-test-text (format "Fluid section %02d" index))
:border t)))) :border '(1 solid)))))
buffer) buffer)
(unwind-protect (unwind-protect
(progn (progn
@ -2611,7 +2607,7 @@
(ebox-test-text content) (ebox-test-text content)
:width 'viewport :width 'viewport
:wrap-mode 'word :wrap-mode 'word
:border t)))) :border '(1 solid)))))
buffer) buffer)
(unwind-protect (unwind-protect
(progn (progn
@ -2645,7 +2641,7 @@
(ebox-test-text content) (ebox-test-text content)
:width 'viewport :width 'viewport
:wrap-mode 'word :wrap-mode 'word
:border t)))) :border '(1 solid)))))
(slot-probe-count 0) (slot-probe-count 0)
(original (symbol-function 'ebox-buffer--rendered-in-existing-slots)) (original (symbol-function 'ebox-buffer--rendered-in-existing-slots))
buffer) buffer)
@ -3712,8 +3708,7 @@
(ordinary-height-nodes (ordinary-height-nodes
(cl-loop for (property value) (cl-loop for (property value)
in '((:height (viewport-height)) in '((:height (viewport-height))
(:min-height (- (viewport-height) 1)) (:max-height (viewport-height)))
(:max-height (+ (viewport-height) 1)))
collect collect
(apply #'ebox-test-box (apply #'ebox-test-box
(ebox-test-text "Ordinary") (ebox-test-text "Ordinary")
@ -3721,8 +3716,7 @@
(flex-height-nodes (flex-height-nodes
(cl-loop for (property value) (cl-loop for (property value)
in '((:height (viewport-height)) in '((:height (viewport-height))
(:min-height (- (viewport-height) 1)) (:max-height (viewport-height)))
(:max-height (+ (viewport-height) 1)))
collect collect
(apply #'ebox-test-flex (apply #'ebox-test-flex
(append (append
@ -4148,7 +4142,7 @@
"Render cache plist signatures should be built linearly." "Render cache plist signatures should be built linearly."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let ((node (ebox-test-root (let ((node (ebox-test-root
(ebox-test-box (ebox-test-text "Signature") :width 80 :border t))) (ebox-test-box (ebox-test-text "Signature") :width 80 :border '(1 solid))))
(original-append (symbol-function 'append)) (original-append (symbol-function 'append))
(append-calls 0)) (append-calls 0))
(cl-letf (((symbol-function 'append) (cl-letf (((symbol-function 'append)
@ -4234,9 +4228,9 @@
(ert-deftest ebox-transparent-preformatted-box-fast-path-is-exact () (ert-deftest ebox-transparent-preformatted-box-fast-path-is-exact ()
"Transparent exact-width wrappers should match the generic box pipeline." "Transparent exact-width wrappers should match the generic box pipeline."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((child-a (ebox-test-box (ebox-test-text "A") :width '(viewport) :border t (let* ((child-a (ebox-test-box (ebox-test-text "A") :width '(viewport) :border '(1 solid)
:region-id 'child-a)) :region-id 'child-a))
(child-b (ebox-test-box (ebox-test-text "B") :width '(viewport) :border t (child-b (ebox-test-box (ebox-test-text "B") :width '(viewport) :border '(1 solid)
:region-id 'child-b)) :region-id 'child-b))
(parent (ebox-test-column (parent (ebox-test-column
:width '(viewport) :region-id 'parent child-a child-b)) :width '(viewport) :region-id 'parent child-a child-b))
@ -4343,11 +4337,12 @@
(ebox-test-box (ebox-test-box
(ebox-test-text (nth 0 case)) :width '(120) :height 5 (ebox-test-text (nth 0 case)) :width '(120) :height 5
:box-sizing 'border-box :overflow (nth 1 case) :box-sizing 'border-box :overflow (nth 1 case)
:vertical-align (nth 2 case) :wrap-mode 'none)) :wrap-mode 'none))
(box (ebox-test-root box-input)) (box (ebox-test-root box-input))
(calls 0) (calls 0)
(original (original
(symbol-function 'ebox--window-render-flat-wrapper-chunk-lines))) (symbol-function 'ebox--window-render-flat-wrapper-chunk-lines)))
(plist-put box :vertical-align (nth 2 case))
(plist-put box :ebox-content-width-exact-p t) (plist-put box :ebox-content-width-exact-p t)
(cl-letf (((symbol-function 'ebox--window-render-flat-wrapper-chunk-lines) (cl-letf (((symbol-function 'ebox--window-render-flat-wrapper-chunk-lines)
(lambda (&rest arguments) (lambda (&rest arguments)
@ -4756,12 +4751,12 @@
(should-not (text-property-search-forward 'ebox-bb left-id t)) (should-not (text-property-search-forward 'ebox-bb left-id t))
(let ((full-rerenders (let ((full-rerenders
(ebox-test--count-full-surface-updates (ebox-test--count-full-surface-updates
(ebox-test--region-update left-id (ebox-test--region-update
:border-bottom-p t left-id :border-bottom '(1 solid "blue")))))
:border-bottom-color "blue"))))
(should (= full-rerenders 0))) (should (= full-rerenders 0)))
(let ((report (ebox-test--buffer-update-report))) (let ((report (ebox-test--buffer-update-report)))
(should (eq (plist-get report :strategy) 'paint-patch)) (should (memq (plist-get report :strategy)
'(span-patch owner-rerender)))
(should-not (plist-get report :root-rerender))) (should-not (plist-get report :root-rerender)))
(let (bottom-face) (let (bottom-face)
(save-excursion (save-excursion
@ -4806,9 +4801,8 @@
(save-restriction (save-restriction
(goto-char (1+ (point-min))) (goto-char (1+ (point-min)))
(narrow-to-region (point) (point-max)) (narrow-to-region (point) (point-max))
(ebox-test--region-update left-id (ebox-test--region-update
:border-bottom-p t left-id :border-bottom '(1 solid "blue")))
:border-bottom-color "blue"))
(should (equal old-metadata (should (equal old-metadata
(plist-get old-state :surface-fragments))) (plist-get old-state :surface-fragments)))
(should (cl-every (lambda (fragment) (should (cl-every (lambda (fragment)
@ -4818,9 +4812,9 @@
(ert-deftest ebox-horizontal-border-removal-preserves-other-border-colors () (ert-deftest ebox-horizontal-border-removal-preserves-other-border-colors ()
"Removing top/bottom border surfaces must not corrupt other border faces." "Removing top/bottom border surfaces must not corrupt other border faces."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(dolist (case '((:border-top-p bt (bb bl br)) (dolist (case '(((:border-top-p nil) bt (bb bl br))
(:border-bottom-p bb (bt bl br)))) ((:border-bottom-width 0) bb (bt bl br))))
(let* ((property (nth 0 case)) (let* ((update (nth 0 case))
(removed-role (nth 1 case)) (removed-role (nth 1 case))
(kept-roles (nth 2 case)) (kept-roles (nth 2 case))
(layout (ebox-test-box (ebox-test-text "Border") (layout (ebox-test-box (ebox-test-text "Border")
@ -4841,34 +4835,46 @@
(setq face (get-text-property (point) 'face))) (setq face (get-text-property (point) 'face)))
(forward-char 1))) (forward-char 1)))
face))) face)))
(ebox-test--region-update region-id property nil) (apply #'ebox-test--region-update region-id update)
(should-not (face-at-role removed-role)) (should-not (face-at-role removed-role))
(dolist (role kept-roles) (dolist (role kept-roles)
(should (should
(ebox-test--border-face-has-color-p (ebox-test--border-face-has-color-p
(face-at-role role) role "#D97757")))))))) (face-at-role role) role "#D97757"))))))))
(ert-deftest ebox-side-border-color-patch-toggles-default-paint-exactly () (ert-deftest ebox-horizontal-border-capability-error-preserves-mounted-state ()
"Side color patches should match default and explicit full-render paint." "An unsupported horizontal width must fail before replacing publication."
(ebox-test--reset-runtime-state)
(let* ((input
(ebox-test-box :id "root" (ebox-test-text "Border")
:border '(1 solid "#D97757")))
(buffer (ebox-render-to-buffer
(generate-new-buffer-name " *ebox-border-capability*")
input))
(state (ebox--buffer-render-state buffer))
(text (with-current-buffer buffer (buffer-string))))
(unwind-protect
(progn
(should-error
(ebox-selector-update-buffer
buffer "#root" :border-top '(2 solid "#D97757")))
(should (eq state (ebox--buffer-render-state buffer)))
(should (equal-including-properties
text (with-current-buffer buffer (buffer-string)))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-side-border-color-patch-replaces-paint-exactly ()
"Side color replacements should match the full-render paint result."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((layout (let* ((layout
(ebox-test-box (ebox-test-box
(ebox-test-text "Border") :width 80 (ebox-test-text "Border") :width 80
:border-left-pixel 2 :border-left-color "#AA0000" :border-left-width 2 :border-left-style 'solid
:border-right-pixel 2 :border-right-color "#AA0000")) :border-left-color "#AA0000"
:border-right-width 2 :border-right-style 'solid
:border-right-color "#AA0000"))
(region-id (car (ebox-region-ids layout)))) (region-id (car (ebox-region-ids layout))))
(ebox-test--with-rendered-buffer layout (ebox-test--with-rendered-buffer layout
(ebox-test--region-update region-id
:border-left-color nil
:border-right-color nil)
(dolist (role '(bl br))
(let ((face
(ebox-test--face-at-property
(buffer-string) (alist-get role ebox-region-types))))
(should
(ebox-test--face-has-key-value-p face :inverse-video t))
(should-not (ebox-test--face-has-key-p face :background))))
(should (ebox-test--buffer-visually-matches-runtime-render-p))
(ebox-test--region-update region-id (ebox-test--region-update region-id
:border-left-color "#00AA00" :border-left-color "#00AA00"
:border-right-color "#00AA00") :border-right-color "#00AA00")
@ -4886,7 +4892,7 @@
"Runtime rerender should resolve viewport-sized boxes with the new context." "Runtime rerender should resolve viewport-sized boxes with the new context."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((node (let ((ebox-viewport-width 240)) (let* ((node (let ((ebox-viewport-width 240))
(ebox-test-box (ebox-test-text "Viewport") :width '(viewport) :border t))) (ebox-test-box (ebox-test-text "Viewport") :width '(viewport) :border '(1 solid))))
buffer) buffer)
(unwind-protect (unwind-protect
(progn (progn
@ -5509,8 +5515,9 @@
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((layout (let* ((layout
(ebox-test-box :id "root" :height 4 :box-sizing 'border-box :overflow (ebox-test-box :id "root" :height 4 :box-sizing 'border-box :overflow
'scroll :padding-top 1 :padding-bottom 1 :border-top-p 'scroll :padding-top 1 :padding-bottom 1 :border-top-p
t :border-top-color "red" :border-bottom-p t t :border-top-color "red"
:border-bottom-width 1 :border-bottom-style 'solid
:border-bottom-color "blue" :border-bottom-color "blue"
(ebox-test-column (ebox-test-column
(ebox-test-box :id "a" (ebox-test-text "A") :height 1) (ebox-test-box :id "a" (ebox-test-text "A") :height 1)
@ -6175,7 +6182,7 @@
:text-align 'left)) :text-align 'left))
(right (ebox-test-box (ebox-test-text "Right") :width '(80) :height 1)) (right (ebox-test-box (ebox-test-text "Right") :width '(80) :height 1))
(layout (ebox-test-row left right)) (layout (ebox-test-row left right))
(left-id (car (ebox-region-ids layout)))) (left-id (car (ebox-region-ids left))))
(ebox-test--with-rendered-buffer layout (ebox-test--with-rendered-buffer layout
(ebox-test--region-update left-id :text-align 'center) (ebox-test--region-update left-id :text-align 'center)
(let ((report (ebox-test--buffer-update-report))) (let ((report (ebox-test--buffer-update-report)))
@ -6189,6 +6196,51 @@
(buffer-substring-no-properties (point-min) (point-max)))) (buffer-substring-no-properties (point-min) (point-max))))
(should (ebox-test--buffer-propertized-matches-runtime-render-p))))) (should (ebox-test--buffer-propertized-matches-runtime-render-p)))))
(ert-deftest ebox-region-update-completes-border-width-style-pairs ()
"A one-property border delta should retain its computed counterpart."
(ebox-test--reset-runtime-state)
(let* ((input
(ebox-test-box (ebox-test-text "Border") :width 80
:border-left '(2 none "#AA0000")))
(region-id (car (ebox-region-ids input))))
(ebox-test--with-rendered-buffer input
(ebox-test--region-update region-id :border-left-style 'solid)
(let ((box (gethash region-id (ebox--buffer-region-box-table
(current-buffer)))))
(should (= (ebox-get box :border-left-pixel) 2)))
(ebox-test--region-update region-id :border-left-width 3)
(let ((box (gethash region-id (ebox--buffer-region-box-table
(current-buffer)))))
(should (= (ebox-get box :border-left-pixel) 3)))
(ebox-test--region-update region-id :border-left-style 'none)
(let ((box (gethash region-id (ebox--buffer-region-box-table
(current-buffer)))))
(should (= (ebox-get box :border-left-pixel) 0)))
(should (ebox-test--buffer-propertized-matches-runtime-render-p)))))
(ert-deftest ebox-region-update-projects-outer-and-layout-config ()
"Region updates should project structural and LayoutConfig facts once."
(ebox-test--reset-runtime-state)
(let* ((input
(ebox-test-row :id "axis" :outer 'inline :item-gap 1
(ebox-test-box (ebox-test-text "A"))
(ebox-test-box (ebox-test-text "B"))))
(buffer (ebox-render-to-buffer
(generate-new-buffer-name " *ebox-axis-update*") input)))
(unwind-protect
(progn
(ebox-selector-update-buffer
buffer "#axis" :outer 'block :item-gap 3)
(let* ((root (plist-get (ebox--buffer-render-state buffer)
:root-node))
(props (ebox-layout-config-props
(ebox-box-node-layout root))))
(should (equal (plist-get root :display) '(block row)))
(should (= (plist-get props :item-gap) 3))
(should (with-current-buffer buffer
(ebox-test--buffer-propertized-matches-runtime-render-p)))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-region-update-border-box-inline-change-uses-span-patch () (ert-deftest ebox-region-update-border-box-inline-change-uses-span-patch ()
"Border-box left/right spacing can be internal when outer width is fixed." "Border-box left/right spacing can be internal when outer width is fixed."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
@ -6196,7 +6248,7 @@
:box-sizing 'border-box)) :box-sizing 'border-box))
(region-id (car (ebox-region-ids box)))) (region-id (car (ebox-region-ids box))))
(ebox-test--with-rendered-buffer box (ebox-test--with-rendered-buffer box
(ebox-test--region-update region-id :padding-left-pixel 8) (ebox-test--region-update region-id :padding-left '(8))
(let ((report (ebox-test--buffer-update-report))) (let ((report (ebox-test--buffer-update-report)))
(should (eq (plist-get report :strategy) 'span-patch)) (should (eq (plist-get report :strategy) 'span-patch))
(should (equal (plist-get report :patch-ops) '(span-patch))) (should (equal (plist-get report :patch-ops) '(span-patch)))
@ -6209,7 +6261,7 @@
:box-sizing 'content-box)) :box-sizing 'content-box))
(region-id (car (ebox-region-ids box)))) (region-id (car (ebox-region-ids box))))
(ebox-test--with-rendered-buffer box (ebox-test--with-rendered-buffer box
(ebox-test--region-update region-id :padding-left-pixel 8) (ebox-test--region-update region-id :padding-left '(8))
(let ((report (ebox-test--buffer-update-report))) (let ((report (ebox-test--buffer-update-report)))
(should (eq (plist-get report :strategy) 'owner-rerender)) (should (eq (plist-get report :strategy) 'owner-rerender))
(should (equal (plist-get report :patch-ops) '(owner-rerender))) (should (equal (plist-get report :patch-ops) '(owner-rerender)))
@ -6224,7 +6276,7 @@
:wrap-mode 'char)) :wrap-mode 'char))
(region-id (car (ebox-region-ids box)))) (region-id (car (ebox-region-ids box))))
(ebox-test--with-rendered-buffer box (ebox-test--with-rendered-buffer box
(ebox-test--region-update region-id :padding-left-pixel 100) (ebox-test--region-update region-id :padding-left '(100))
(let ((report (ebox-test--buffer-update-report))) (let ((report (ebox-test--buffer-update-report)))
(should (memq (plist-get report :strategy) (should (memq (plist-get report :strategy)
'(span-patch owner-rerender))) '(span-patch owner-rerender)))
@ -6233,27 +6285,35 @@
(should-not (plist-get report :tp-scope-fallback)) (should-not (plist-get report :tp-scope-fallback))
(should (ebox-test--buffer-propertized-matches-runtime-render-p)))))) (should (ebox-test--buffer-propertized-matches-runtime-render-p))))))
(ert-deftest ebox-region-update-matrix-covers-all-box-longhands () (ert-deftest ebox-region-update-matrix-uses-only-public-properties ()
"The update tests should classify every non-content box longhand." "Region update coverage must not preserve private engine spellings."
(let ((covered (append (ebox-test--region-update-case-properties (dolist (property
ebox-test--region-update-paint-cases) (append (ebox-test--region-update-case-properties
(ebox-test--region-update-case-properties ebox-test--region-update-paint-cases)
ebox-test--region-update-geometry-cases)))) (ebox-test--region-update-case-properties
(should-not (cl-set-difference ebox--region-update-longhand-props ebox-test--region-update-geometry-cases)))
covered (should (or (memq property ebox--region-update-control-properties)
:test #'eq)) (ebox-style-schema-id property))))
(should-not (cl-set-difference covered (dolist (property '(:padding-left-pixel :padding-right-pixel
ebox--region-update-longhand-props :padding-top-height :padding-bottom-height
:test #'eq)) :margin-left-pixel :margin-right-pixel
(should-not (cl-set-difference ebox--paint-style-signature-keys :margin-top-height :margin-bottom-height
(ebox-test--region-update-case-properties :border-left-pixel :border-right-pixel
ebox-test--region-update-paint-cases) :border-top-pixel :border-bottom-pixel
:test #'eq)))) :border-left-style :border-right-style
:border-top-style :border-bottom-style
:vertical-align))
(should-not
(memq property
(append (ebox-test--region-update-case-properties
ebox-test--region-update-paint-cases)
(ebox-test--region-update-case-properties
ebox-test--region-update-geometry-cases))))))
(ert-deftest ebox-region-update-all-paint-longhands-use-paint-patch () (ert-deftest ebox-region-update-all-paint-longhands-use-paint-patch ()
"Every paint longhand should patch text properties without replacing spans." "Every paint longhand should patch text properties without replacing spans."
(dolist (case ebox-test--region-update-paint-cases) (dolist (case ebox-test--region-update-paint-cases)
(pcase-let ((`(,property ,value ,expected) case)) (pcase-let ((`(,property ,value ,engine-property ,expected) case))
(ert-info ((format "property %S" property)) (ert-info ((format "property %S" property))
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((box (ebox-test-box (ebox-test-text "Paint") :width '(120) (let* ((box (ebox-test-box (ebox-test-text "Paint") :width '(120)
@ -6270,7 +6330,8 @@
(should (eq (gethash region-id ebox--region-box-table) (should (eq (gethash region-id ebox--region-box-table)
runtime-box)) runtime-box))
(should-not (eq runtime-box box)) (should-not (eq runtime-box box))
(should (equal (ebox-get runtime-box property) expected))) (should (equal (ebox-get runtime-box engine-property)
expected)))
(let ((report (ebox-test--buffer-update-report))) (let ((report (ebox-test--buffer-update-report)))
(should (eq (plist-get report :strategy) 'paint-patch)) (should (eq (plist-get report :strategy) 'paint-patch))
(should (equal (plist-get report :patch-ops) (should (equal (plist-get report :patch-ops)
@ -6281,7 +6342,8 @@
(ert-deftest ebox-region-update-all-geometry-longhands-use-minimal-strategy () (ert-deftest ebox-region-update-all-geometry-longhands-use-minimal-strategy ()
"Every geometry longhand should use a correct scoped non-root strategy." "Every geometry longhand should use a correct scoped non-root strategy."
(dolist (case ebox-test--region-update-geometry-cases) (dolist (case ebox-test--region-update-geometry-cases)
(pcase-let ((`(,property ,value ,expected ,_strategy) case)) (pcase-let
((`(,property ,value ,engine-property ,expected ,_strategy) case))
(ert-info ((format "property %S" property)) (ert-info ((format "property %S" property))
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((box (ebox-test-box (ebox-test-text "Alpha\nBeta\nGamma\nDelta") (let* ((box (ebox-test-box (ebox-test-text "Alpha\nBeta\nGamma\nDelta")
@ -6301,10 +6363,11 @@
(should (eq (gethash region-id ebox--region-box-table) (should (eq (gethash region-id ebox--region-box-table)
runtime-box)) runtime-box))
(should-not (eq runtime-box box)) (should-not (eq runtime-box box))
(should (equal (ebox-get runtime-box property) expected))) (should (equal (ebox-get runtime-box engine-property)
expected)))
(let ((report (ebox-test--buffer-update-report))) (let ((report (ebox-test--buffer-update-report)))
(should (memq (plist-get report :strategy) (should (memq (plist-get report :strategy)
'(span-patch owner-rerender))) '(paint-patch span-patch owner-rerender)))
(should (equal (plist-get report :dirty-kinds) '(geometry))) (should (equal (plist-get report :dirty-kinds) '(geometry)))
(should (eq (plist-get report :owner-type) 'box)) (should (eq (plist-get report :owner-type) 'box))
(should-not (plist-get report :tp-full-root)) (should-not (plist-get report :tp-full-root))
@ -6373,43 +6436,6 @@
(should (equal (plist-get report :dirty-kinds) '(paint))) (should (equal (plist-get report :dirty-kinds) '(paint)))
(should-not (plist-get report :full-rerender))))))) (should-not (plist-get report :full-rerender)))))))
(ert-deftest ebox-vertical-align-middle-alias-matches-center ()
"Creation and updates should normalize middle to center before rendering."
(ebox-test--reset-runtime-state)
(let ((middle (ebox-test-box (ebox-test-text "X") :height 3
:vertical-align 'middle)))
(should (equal (ebox--expand-property :vertical-align 'middle)
'(:vertical-align center)))
(should (eq (ebox-get (ebox-test-root middle) :vertical-align)
'center))
(should (= (cl-position-if
(lambda (line)
(string-match-p "X" (substring-no-properties line)))
(ebox-string-lines (ebox-render middle)))
1))
(let* ((updated (ebox-test-box (ebox-test-text "X") :height 3
:vertical-align 'top))
(region-id (car (ebox-region-ids updated))))
(ebox-test--with-rendered-buffer updated
(goto-char (point-min))
(search-forward "X")
(should (= (line-number-at-pos (match-beginning 0)) 1))
(ebox-test--region-update region-id :vertical-align 'middle)
(should (eq (ebox-style-node-specified-value
updated :vertical-align)
'top))
(should (eq (ebox-get (gethash region-id
(ebox--buffer-region-box-table
(current-buffer)))
:vertical-align)
'center))
(goto-char (point-min))
(search-forward "X")
(should (= (line-number-at-pos (match-beginning 0)) 2))
(should (eq (plist-get (ebox-test--buffer-update-report) :strategy)
'span-patch))
(should (ebox-test--buffer-visually-matches-runtime-render-p))))))
(ert-deftest ebox-update-reuses-shorthand-property-expansion () (ert-deftest ebox-update-reuses-shorthand-property-expansion ()
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((box (ebox-test-box (ebox-test-text "Box") :width 60)) (let* ((box (ebox-test-box (ebox-test-text "Box") :width 60))
@ -6417,7 +6443,7 @@
(ebox-test--with-rendered-buffer box (ebox-test--with-rendered-buffer box
(ebox-test--region-update region-id (ebox-test--region-update region-id
:padding '(1 2) :padding '(1 2)
:border '(2 "#abcdef") :border '(1 "#abcdef")
:margin-inline 3) :margin-inline 3)
(let ((runtime-box (let ((runtime-box
(gethash region-id (ebox--buffer-region-box-table (gethash region-id (ebox--buffer-region-box-table
@ -6426,8 +6452,7 @@
(should (= (ebox-get runtime-box :padding-top-height) 1)) (should (= (ebox-get runtime-box :padding-top-height) 1))
(should (= (ebox-get runtime-box :padding-right-pixel) (should (= (ebox-get runtime-box :padding-right-pixel)
(* 2 (ebox--space-pixel-width)))) (* 2 (ebox--space-pixel-width))))
(should (= (ebox-get runtime-box :border-left-pixel) (should (= (ebox-get runtime-box :border-left-pixel) 1))
(* 2 (ebox--space-pixel-width))))
(should (equal (ebox-get runtime-box :border-right-color) "#abcdef")) (should (equal (ebox-get runtime-box :border-right-color) "#abcdef"))
(should (= (ebox-get runtime-box :margin-left-pixel) (should (= (ebox-get runtime-box :margin-left-pixel)
(* 3 (ebox--space-pixel-width)))))))) (* 3 (ebox--space-pixel-width))))))))
@ -6461,8 +6486,7 @@
(should (= (ebox-get runtime-box :padding-top-height) 1)) (should (= (ebox-get runtime-box :padding-top-height) 1))
(should (= (ebox-get runtime-box :padding-right-pixel) (should (= (ebox-get runtime-box :padding-right-pixel)
(* 2 (ebox--space-pixel-width)))) (* 2 (ebox--space-pixel-width))))
(should (= (ebox-get runtime-box :border-left-pixel) (should (= (ebox-get runtime-box :border-left-pixel) 1))
(ebox--space-pixel-width)))
(should (equal (ebox-get runtime-box :border-right-color) "#abcdef")) (should (equal (ebox-get runtime-box :border-right-color) "#abcdef"))
(should (= (ebox-get runtime-box :margin-left-pixel) (should (= (ebox-get runtime-box :margin-left-pixel)
(* 3 (ebox--space-pixel-width))))) (* 3 (ebox--space-pixel-width)))))
@ -6585,15 +6609,13 @@
(ebox-test--region-update region-id :color "#fedcba") (ebox-test--region-update region-id :color "#fedcba")
(ebox-test--assert-visible-overflow-paint (ebox-test--assert-visible-overflow-paint
(buffer-string) "#fedcba" "#123456" region-id) (buffer-string) "#fedcba" "#123456" region-id)
(ebox-test--region-update region-id :color nil) (let ((published (buffer-string)))
(cl-loop for line in (ebox-string-lines (buffer-string)) (should-error
for glyph in '(?A ?B ?C) (ebox-test--region-update region-id :color nil)
for glyph-pos = (cl-position glyph line) :type 'error)
do (should (integerp glyph-pos)) (should (equal-including-properties published (buffer-string)))
do (should-not (ebox-test--assert-visible-overflow-paint
(ebox-test--face-has-key-p (buffer-string) "#fedcba" "#123456" region-id)))))
(get-text-property glyph-pos 'face line)
:foreground))))))
(ert-deftest ebox-owner-rerender-keeps-visible-overflow-unowned () (ert-deftest ebox-owner-rerender-keeps-visible-overflow-unowned ()
"Owner rerenders should not restore box ownership to visible overflow." "Owner rerenders should not restore box ownership to visible overflow."
@ -6856,7 +6878,6 @@
:width 140 :width 140
:height 5 :height 5
:overflow 'scroll :overflow 'scroll
:vertical-align 'top
:color "#112233" :color "#112233"
:bgcolor "#ddeeff")) :bgcolor "#ddeeff"))
(region-id (ebox--ensure-region-id (ebox-test-root box))) (region-id (ebox--ensure-region-id (ebox-test-root box)))
@ -6864,7 +6885,6 @@
:width 140 :width 140
:height 5 :height 5
:overflow 'hidden :overflow 'hidden
:vertical-align 'top
:color "#112233" :color "#112233"
:bgcolor "#ddeeff")) :bgcolor "#ddeeff"))
(expected (progn (expected (progn
@ -6937,7 +6957,7 @@
(ebox-test-box (ebox-test-text "") :width '(viewport) (ebox-test-box (ebox-test-text "") :width '(viewport)
:height 1) :height 1)
(ebox-test-box :id "root" :width '(viewport) :height (ebox-test-box :id "root" :width '(viewport) :height
'(- (viewport-height) 2) :overflow '(viewport-height) :overflow
'scroll 'scroll
(apply #'ebox-test-column (apply #'ebox-test-column
(append (list :width '(viewport) :padding '(1 (8))) (append (list :width '(viewport) :padding '(1 (8)))
@ -6979,7 +6999,7 @@
:rendered-lines :rendered-lines
(length (plist-get state (length (plist-get state
:rendered-content-lines))))) :rendered-content-lines)))))
(should (= (plist-get state :content-height) 12))) (should (= (plist-get state :content-height) 14)))
(when-let* ((materialize (when-let* ((materialize
(plist-get state :materialize-content-lines))) (plist-get state :materialize-content-lines)))
(plist-put (plist-put
@ -7008,7 +7028,7 @@
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((layout (let* ((layout
(ebox-test-box :id "root" :width '(viewport) :height (ebox-test-box :id "root" :width '(viewport) :height
'(- (viewport-height) 2) :overflow 'scroll '(viewport-height) :overflow 'scroll
(apply #'ebox-test-column (apply #'ebox-test-column
(append (list :width '(viewport)) (append (list :width '(viewport))
(cl-loop for idx below 40 collect (cl-loop for idx below 40 collect
@ -7026,7 +7046,7 @@
(ebox--intrinsic-layout-measurement t)) (ebox--intrinsic-layout-measurement t))
(setq rendered (ebox-render layout))) (setq rendered (ebox-render layout)))
(should (= calls 0)) (should (= calls 0))
(should (= (ebox-string-height rendered) 10)) (should (= (ebox-string-height rendered) 12))
(let ((ebox-viewport-width 240) (let ((ebox-viewport-width 240)
(ebox-viewport-height 12) (ebox-viewport-height 12)
(ebox--intrinsic-layout-measurement nil)) (ebox--intrinsic-layout-measurement nil))
@ -7038,7 +7058,7 @@
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((layout (let* ((layout
(ebox-test-box :id "root" :width '(viewport) :height (ebox-test-box :id "root" :width '(viewport) :height
'(- (viewport-height) 10) :overflow 'scroll '(viewport-height) :overflow 'scroll
(apply #'ebox-test-column (apply #'ebox-test-column
(append (list :width '(viewport)) (append (list :width '(viewport))
(cl-loop for idx below 80 collect (cl-loop for idx below 80 collect
@ -7084,7 +7104,7 @@
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((layout (let* ((layout
(ebox-test-box :id "root" :width '(viewport) :height (ebox-test-box :id "root" :width '(viewport) :height
'(- (viewport-height) 10) :overflow 'scroll '(viewport-height) :overflow 'scroll
(apply #'ebox-test-column (apply #'ebox-test-column
(append (list :width '(viewport)) (append (list :width '(viewport))
(cl-loop for idx below 80 collect (cl-loop for idx below 80 collect
@ -7148,7 +7168,7 @@
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
(let* ((layout (let* ((layout
(ebox-test-box :id "root" :width '(viewport) :height (ebox-test-box :id "root" :width '(viewport) :height
'(- (viewport-height) 10) :overflow 'scroll '(viewport-height) :overflow 'scroll
(apply #'ebox-test-column (apply #'ebox-test-column
(append (list :width '(viewport)) (append (list :width '(viewport))
(cl-loop for idx below 80 collect (cl-loop for idx below 80 collect
@ -7335,36 +7355,6 @@
(should (equal expected-signatures actual-signatures)) (should (equal expected-signatures actual-signatures))
(should-not producer))) (should-not producer)))
(ert-deftest ebox-flex-window-aligned-wrapper-falls-back-to-full-prefix ()
"Non-left-aligned flex prefixes should use the full-render fallback."
(ebox-test--reset-runtime-state)
(let* ((node
(apply #'ebox-test-flex
(append
(list :width '(100) :text-align 'center :flex-flow '(row wrap)
:row-gap 0 :column-gap 0)
(cl-loop for char across "ABCDEF" collect
(ebox-test-box (ebox-test-text (char-to-string char)) :width
'(50) :height 1)))))
(full-lines (ebox-string-lines (ebox-render node)))
(expected-signatures
(mapcar #'ebox-test--visual-line-signature
(seq-take full-lines 3)))
(ebox--render-cache-table (make-hash-table :test 'equal))
(ebox--render-cache-signature-cache (make-hash-table :test 'eq))
(first (ebox-test--call-input-node
#'ebox--render-stack-window-lines node 1))
(extended
(ebox-test--call-input-node
#'ebox--render-stack-window-lines node 3 (plist-get first :cache)))
(producer (ebox-test--call-input-node
#'ebox--render-node-window-lines node 3))
(actual-signatures
(mapcar #'ebox-test--visual-line-signature
(plist-get extended :lines))))
(should (equal expected-signatures actual-signatures))
(should-not producer)))
(ert-deftest ebox-canonical-column-scroll-window-renders-a-bounded-prefix () (ert-deftest ebox-canonical-column-scroll-window-renders-a-bounded-prefix ()
"A canonical Column should expose only the requested scroll prefix." "A canonical Column should expose only the requested scroll prefix."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)
@ -7809,27 +7799,29 @@
(= 1 (= 1
(ebox--flex-window-completion-prefix-tail-rewrite-count (ebox--flex-window-completion-prefix-tail-rewrite-count
(list :box (ebox-test-root (list :box (ebox-test-root
(ebox-test-box :border-bottom-p t (ebox-test-box :border-bottom-width 1
:border-bottom-style 'solid
:padding-bottom 0)))))) :padding-bottom 0))))))
(should (should
(= 1 (= 1
(ebox--flex-window-completion-prefix-tail-rewrite-count (ebox--flex-window-completion-prefix-tail-rewrite-count
(list :box (ebox-test-root (list :box (ebox-test-root
(ebox-test-box :border-bottom-p t (ebox-test-box :border-bottom-width 1
:border-bottom-style 'solid
:padding-bottom 0 :padding-bottom 0
:margin-bottom 2)))))) :margin-bottom 2))))))
(should (should
(= 0 (= 0
(ebox--flex-window-completion-prefix-tail-rewrite-count (ebox--flex-window-completion-prefix-tail-rewrite-count
(list :box (ebox-test-root (list :box (ebox-test-root
(ebox-test-box :border-bottom-p t (ebox-test-box :border-bottom-width 1
:border-bottom-style 'solid
:padding-bottom 2)))))) :padding-bottom 2))))))
(should (should
(= 0 (= 0
(ebox--flex-window-completion-prefix-tail-rewrite-count (ebox--flex-window-completion-prefix-tail-rewrite-count
(list :box (ebox-test-root (list :box (ebox-test-root
(ebox-test-box :border-bottom-p nil (ebox-test-box :padding-bottom 0)))))))
:padding-bottom 0)))))))
(ert-deftest ebox-flex-window-cache-rejects-short-incomplete-exact-hit () (ert-deftest ebox-flex-window-cache-rejects-short-incomplete-exact-hit ()
"An underfilled exact-limit cache must resume from its prefix seed." "An underfilled exact-limit cache must resume from its prefix seed."
@ -11253,8 +11245,8 @@ explicit visible-window handoff may admit its full-content publication."
(ebox-test-box :id "width-only" :width '(viewport)) (ebox-test-box :id "width-only" :width '(viewport))
(ebox-test-box :id "height-only" :height (ebox-test-box :id "height-only" :height
'(viewport-height)) '(viewport-height))
(ebox-test-flex :id "flex-height" :min-height (ebox-test-flex :id "flex-height" :height
'(- (viewport-height) 1) '(viewport-height)
(ebox-test-box (ebox-test-text "Flex"))))))) (ebox-test-box (ebox-test-text "Flex")))))))
(let* ((state (ebox--buffer-render-state buffer)) (let* ((state (ebox--buffer-render-state buffer))
(root (plist-get state :root-node)) (root (plist-get state :root-node))
@ -12475,18 +12467,18 @@ explicit visible-window handoff may admit its full-content publication."
(let* ((box (ebox-test-box (ebox-test-text "Atomic") (let* ((box (ebox-test-box (ebox-test-text "Atomic")
:width '(120) :width '(120)
:bgcolor "#111111" :bgcolor "#111111"
:padding-left-pixel 2)) :padding-left 2))
(region-id (car (ebox-region-ids box)))) (region-id (car (ebox-region-ids box))))
(ebox-test--with-rendered-buffer box (ebox-test--with-rendered-buffer box
(should-error (should-error
(ebox-test--region-update region-id (ebox-test--region-update region-id
:bgcolor "#222222" :bgcolor "#222222"
:padding-left-pixel -1) :padding-left -1)
:type 'error) :type 'error)
(should (equal (ebox-style-node-specified-value box :bgcolor) (should (equal (ebox-style-node-specified-value box :bgcolor)
"#111111")) "#111111"))
(should (= (ebox-style-node-specified-value (should (= (ebox-style-node-specified-value
box :padding-left-pixel) box :padding-left)
2))))) 2)))))
(ert-deftest ebox-region-update-no-op-skips-rollback-snapshot () (ert-deftest ebox-region-update-no-op-skips-rollback-snapshot ()
@ -12724,7 +12716,7 @@ explicit visible-window handoff may admit its full-content publication."
(ebox-test-box (ebox-test-text "Border") (ebox-test-box (ebox-test-text "Border")
:width 60 :width 60
:padding '(1 1) :padding '(1 1)
:border t))) :border '(1 solid))))
(left-face (ebox-test--face-at-property rendered 'ebox-bl)) (left-face (ebox-test--face-at-property rendered 'ebox-bl))
(right-face (ebox-test--face-at-property rendered 'ebox-br)) (right-face (ebox-test--face-at-property rendered 'ebox-br))
(top-face (ebox-test--face-at-property rendered 'ebox-bt)) (top-face (ebox-test--face-at-property rendered 'ebox-bt))
@ -13034,7 +13026,7 @@ publication with properties intact."
(ebox-test-box (ebox-test-text "Header line") :id 'header (ebox-test-box (ebox-test-text "Header line") :id 'header
:width (list 300)) :width (list 300))
(ebox-test-box (ebox-test-text "Secret content") :id 'secret (ebox-test-box (ebox-test-text "Secret content") :id 'secret
:width (list 300) :border t :width (list 300) :border '(1 solid)
:bgcolor "#224466" :color "#FFFFFF") :bgcolor "#224466" :color "#FFFFFF")
(ebox-test-box (ebox-test-text "Footer line") :id 'footer (ebox-test-box (ebox-test-text "Footer line") :id 'footer
:width (list 300)))))) :width (list 300))))))
@ -13072,11 +13064,11 @@ face patch must match that exactly (issue014)."
(lambda (parent-color) (lambda (parent-color)
(ebox-test-box (ebox-test-box
:id 'parent :width (list 400) :id 'parent :width (list 400)
:border (list '(4) 'solid parent-color) :border (list 1 'solid parent-color)
:padding '(1 (12)) :padding '(1 (12))
(ebox-test-column (ebox-test-column
(ebox-test-box :id 'child (ebox-test-text "CHILD") :width (list 200) (ebox-test-box :id 'child (ebox-test-text "CHILD") :width (list 200)
:border '((4) solid "#00AA00")))))) :border '(1 solid "#00AA00"))))))
(visual-runs (visual-runs
(lambda (buffer role) (lambda (buffer role)
(with-current-buffer buffer (with-current-buffer buffer

View File

@ -113,7 +113,7 @@
sum (ebox-dsl-test--tree-count child predicate))))) sum (ebox-dsl-test--tree-count child predicate)))))
(ert-deftest ebox-style-expands-ebox-aliases-to-canonical-longhands () (ert-deftest ebox-style-expands-ebox-aliases-to-canonical-longhands ()
"Existing Ebox aliases should normalize to CSS-like longhand properties." "Approved aliases and shorthands should produce canonical longhands."
(let ((style (ebox-style-compute (let ((style (ebox-style-compute
(list :bgcolor "#ffffff" (list :bgcolor "#ffffff"
:padding '(1 2) :padding '(1 2)
@ -124,7 +124,7 @@
(should (= (plist-get style :padding-inline-end) 2)) (should (= (plist-get style :padding-inline-end) 2))
(should (= (plist-get style :padding-block-end) 1)) (should (= (plist-get style :padding-block-end) 1))
(should (= (plist-get style :padding-inline-start) 2)) (should (= (plist-get style :padding-inline-start) 2))
(should (= (plist-get style :margin-inline-start) 3)) (should (= (plist-get style :margin-left) 3))
(should (equal (plist-get style :border-color) "#333333")))) (should (equal (plist-get style :border-color) "#333333"))))
(ert-deftest ebox-style-classifies-incremental-effects () (ert-deftest ebox-style-classifies-incremental-effects ()
@ -134,8 +134,8 @@
(should (eq (ebox-style-dirty-kind :padding-inline-start) 'geometry)) (should (eq (ebox-style-dirty-kind :padding-inline-start) 'geometry))
(should (eq (ebox-style-dirty-kind :width) 'geometry)) (should (eq (ebox-style-dirty-kind :width) 'geometry))
(should (eq (ebox-style-dirty-kind :outer) 'structure)) (should (eq (ebox-style-dirty-kind :outer) 'structure))
(should (eq (ebox-style-dirty-kind :layout) 'structure)) (should-not (ebox-style-dirty-kind :layout))
(should (eq (ebox-style-dirty-kind :display) 'structure))) (should-not (ebox-style-dirty-kind :display)))
(ert-deftest ebox-typed-form-projects-outer-and-layout-to-display () (ert-deftest ebox-typed-form-projects-outer-and-layout-to-display ()
"A typed Box form should project its two axes to one display pair." "A typed Box form should project its two axes to one display pair."
@ -143,6 +143,14 @@
(should (equal (ebox--computed-display (ebox-dsl-test--root node)) (should (equal (ebox--computed-display (ebox-dsl-test--root node))
'(inline flex))))) '(inline flex)))))
(ert-deftest ebox-inline-outer-participates-in-the-cascade ()
"An inline outer declaration should outrank a matching stylesheet rule."
(let ((ebox-style-stylesheet (ecss-stylesheet-create)))
(ebox-style-add-rule ".card" '(:outer block))
(let ((node (ebox-dsl-test--root
(ebox-build '(box :class "card" :outer inline "A")))))
(should (equal (ebox--computed-display node) '(inline flow))))))
(ert-deftest ebox-style-unrelated-facts-preserve-typed-layout-config () (ert-deftest ebox-style-unrelated-facts-preserve-typed-layout-config ()
"Unrelated computed facts must not replace direct typed layout values." "Unrelated computed facts must not replace direct typed layout values."
(dolist (case '((flex (dolist (case '((flex
@ -174,6 +182,80 @@
(equal (ebox-style-compile-declarations '(:bgcolor "red") t) (equal (ebox-style-compile-declarations '(:bgcolor "red") t)
(ebox-style-compile-declarations '(:background-color "red") t)))) (ebox-style-compile-declarations '(:background-color "red") t))))
(ert-deftest ebox-canonical-property-vocabulary-is-the-only-author-path ()
"The frozen G4b vocabulary should have one canonical spelling and owner."
(dolist (entry '((:font-size . ebox/font-size)
(:font-style . ebox/font-style)
(:text-decoration-line . ebox/text-decoration-line)
(:text-decoration-color . ebox/text-decoration-color)
(:text-decoration-style . ebox/text-decoration-style)
(:item-gap . ebox/item-gap)
(:cross-align . ebox/cross-align)
(:justify-self . ebox/justify-self)))
(should (eq (ebox-style-schema-id (car entry)) (cdr entry))))
(should (eq (ebox-style-schema-id :bgcolor)
(ebox-style-schema-id :background-color)))
(should (eq (ebox-style-schema-id :font-slant)
(ebox-style-schema-id :font-style)))
(dolist (entry '((:padding-top . ebox/padding-top)
(:padding-right . ebox/padding-right)
(:padding-bottom . ebox/padding-bottom)
(:padding-left . ebox/padding-left)
(:margin-top . ebox/margin-top)
(:margin-right . ebox/margin-right)
(:margin-bottom . ebox/margin-bottom)
(:margin-left . ebox/margin-left)))
(should (eq (ebox-style-schema-id (car entry)) (cdr entry))))
(should-not (eq (ebox-style-schema-id :padding-top)
(ebox-style-schema-id :padding-block-start)))
(should-not (eq (ebox-style-schema-id :margin-left)
(ebox-style-schema-id :margin-inline-start)))
(dolist (name '(:font :font-height :border-bottom-p
:grid-row-gap :grid-column-gap :vertical-align
:display :layout :content :surface-properties :face
:padding-top-height :padding-right-pixel
:padding-bottom-height :padding-left-pixel
:margin-top-height :margin-right-pixel
:margin-bottom-height :margin-left-pixel
:border-right-pixel :border-left-pixel))
(should-not (ebox-style-schema-id name)))
(dolist (name '(:border-top-p :grid-column-span :grid-row-span))
(should (plist-get (ebox-style-property name) :shorthand))))
(ert-deftest ebox-normal-text-align-is-not-a-layout-wide-property ()
"Only the Normal Box form owns text alignment author input."
(should (ebox-build '(box :text-align center "A")))
(dolist (tag '(row column flex grid))
(should-error
(ebox-build (list tag :text-align 'center "A"))
:type 'error)))
(ert-deftest ebox-edge-projection-uses-one-ecss-winner-order ()
"Logical and physical edges conflict locally and cascade across sources."
(should-error
(ebox-style-compile-declarations
'(:padding-left (4) :padding-inline-start (8)) t)
:type 'ecss-invalid-declaration)
(let* ((base (ebox-style-compile-declarations '(:padding (0 (8))) t))
(override
(ebox-style-compile-declarations '(:padding-left (24)) t))
(style
(ebox-style-compute-subject
(ecss-subject-create :type "box")
(ebox-style-merge-declarations base override))))
(should (equal (plist-get (ebox-style--box-values style) :padding-left)
'(24))))
(let ((ebox-style-stylesheet (ecss-stylesheet-create)))
(ebox-style-add-rule ".card" '(:margin-inline-start (2)))
(ebox-style-add-rule "#target" '(:margin-left (6)))
(let ((style
(ebox-style-compute-subject
(ecss-subject-create
:type "box" :id "target" :classes '("card"))
nil)))
(should (equal (plist-get (ebox-style--box-values style) :margin-left)
'(6))))))
(ert-deftest ebox-style-rejects-canonical-and-alias-duplicates () (ert-deftest ebox-style-rejects-canonical-and-alias-duplicates ()
"One declaration source should not spell one property twice." "One declaration source should not spell one property twice."
(should-error (should-error
@ -185,22 +267,24 @@
'(:background-color "red" :bgcolor "blue") t) '(:background-color "red" :bgcolor "blue") t)
:type 'error)) :type 'error))
(ert-deftest ebox-style-preserves-ordered-shorthand-longhand-normalization () (ert-deftest ebox-author-declaration-is-one-atomic-normalization-group ()
"Ebox author pairs remain ordered normalization groups before cutover." "One author group rejects overlaps while later cascade groups may override."
(let ((border-then-top (dolist (declarations
(ebox-style-compile-declarations '((:padding 1 :padding-left 2)
'(:border "red" :border-top none) t)) (:border "red" :border-top none)
(top-then-border (:gap 1 :row-gap 2)
(ebox-style-compile-declarations (:flex-flow (row wrap) :flex-wrap nowrap)
'(:border-top none :border "red") t))) (:flex (1 1 auto) :flex-grow 2)
(should (eq 'none (:border-top-p t :border-top-style none)))
(plist-get border-then-top 'ebox/border-top-style))) (should-error
(should (equal "red" (ebox-style-compile-declarations declarations t)
(plist-get border-then-top 'ebox/border-right-color))) :type 'ecss-invalid-declaration))
(should (eq 'solid (should
(plist-get top-then-border 'ebox/border-top-style))) (equal
(should (equal "red" (ebox-style-merge-declarations
(plist-get top-then-border 'ebox/border-top-color))))) (ebox-style-compile-declarations '(:gap 1) t)
(ebox-style-compile-declarations '(:row-gap 2) t))
'(ebox/row-gap 2 ebox/column-gap 1))))
(ert-deftest ebox-style-property-registry-rejects-alias-collisions () (ert-deftest ebox-style-property-registry-rejects-alias-collisions ()
"An alias should never shadow another canonical property." "An alias should never shadow another canonical property."
@ -226,7 +310,7 @@
(setcar (plist-get metadata :contexts) 'damaged) (setcar (plist-get metadata :contexts) 'damaged)
(should (eq :background-color (should (eq :background-color
(plist-get (ebox-style-property :bgcolor) :name))) (plist-get (ebox-style-property :bgcolor) :name)))
(should (equal '(box) (should (equal '(text box)
(plist-get (ebox-style-property :bgcolor) :contexts))))) (plist-get (ebox-style-property :bgcolor) :contexts)))))
(ert-deftest ebox-style-border-present-is-a-typed-shorthand-not-an-alias () (ert-deftest ebox-style-border-present-is-a-typed-shorthand-not-an-alias ()
@ -239,8 +323,49 @@
'(ebox/border-top-width 0 ebox/border-top-style none))) '(ebox/border-top-width 0 ebox/border-top-style none)))
(should (eq (ebox-style-canonical-name :border-top-p) :border-top-p))) (should (eq (ebox-style-canonical-name :border-top-p) :border-top-p)))
(ert-deftest ebox-style-exclusive-check-does-not-reexpand-ordinary-shorthands () (ert-deftest ebox-border-width-unit-is-always-integer-pixels ()
"Conflict validation should leave the declaration cache effective." "Border width should not reuse Box edge column/pixel wrapper grammar."
(should
(equal
(ebox-style-compile-declarations '(:border (1 solid "#687386")) t)
'(ebox/border-top-width 1 ebox/border-top-style solid
ebox/border-top-color "#687386"
ebox/border-right-width 1 ebox/border-right-style solid
ebox/border-right-color "#687386"
ebox/border-bottom-width 1 ebox/border-bottom-style solid
ebox/border-bottom-color "#687386"
ebox/border-left-width 1 ebox/border-left-style solid
ebox/border-left-color "#687386")))
(should-error
(ebox-style-compile-declarations
'(:border ((1) solid "#687386")) t)
:type 'error))
(ert-deftest ebox-border-engine-projection-keeps-exact-pixels ()
"Border projection must not reinterpret pixels as character columns."
(cl-letf (((symbol-function 'ebox--space-pixel-width) (lambda () 7)))
(should
(equal
(ebox-style--expand-engine-plist
'(:border-left-width 1 :border-left-style solid
:border-right-width 2 :border-right-style solid
:padding-left 1 :margin-right (3)))
'(:border-left-pixel 1 :border-left-style solid
:border-right-pixel 2 :border-right-style solid
:padding-left-pixel 7 :margin-right-pixel 3))))
(should
(equal
(ebox-style--expand-engine-plist
'(:border-left-width 2 :border-left-style none))
'(:border-left-pixel 0 :border-left-style none)))
(should
(equal
(ebox-style--expand-engine-plist
'(:border-top-width 2 :border-top-style solid))
'(:border-top-pixel 2 :border-top-style solid))))
(ert-deftest ebox-style-declaration-cache-expands-shorthand-once ()
"A cached declaration should not expand its shorthand again."
(let ((calls 0) (let ((calls 0)
(original (symbol-function 'ebox-style--padding-shorthand))) (original (symbol-function 'ebox-style--padding-shorthand)))
(clrhash ebox-style--declaration-cache) (clrhash ebox-style--declaration-cache)
@ -862,7 +987,7 @@
"The typed Grid constructor should emit one canonical closed property set." "The typed Grid constructor should emit one canonical closed property set."
(let* ((layout (let* ((layout
(ebox-grid-layout-create (ebox-grid-layout-create
:grid-template-columns '((20) 1fr) :grid-template-columns '((20) (fr 1))
:grid-template-rows '(1 auto) :grid-template-rows '(1 auto)
:grid-auto-columns '((30)) :grid-auto-columns '((30))
:grid-auto-rows '(2) :grid-auto-rows '(2)
@ -897,15 +1022,28 @@
(dolist (value ebox--grid-content-alignment-values) (dolist (value ebox--grid-content-alignment-values)
(should (ebox-grid-layout-create :justify-content value)) (should (ebox-grid-layout-create :justify-content value))
(should (ebox-grid-layout-create :align-content value))) (should (ebox-grid-layout-create :align-content value)))
(dolist (value '(min-content max-content))
(should (ebox-grid-layout-create :grid-template-columns (list value)))
(should (ebox-grid-layout-create :grid-template-rows (list value)))
(should (ebox-grid-layout-create :grid-auto-columns value))
(should (ebox-grid-layout-create :grid-auto-rows value)))
(should
(ebox-grid-layout-create
:grid-template-columns '((repeat 2 (auto (fr 1))))))
(dolist (plist '((:grid-auto-flow diagonal) (dolist (plist '((:grid-auto-flow diagonal)
(:grid-template-columns (bogus)) (:grid-template-columns (bogus))
(:grid-template-rows (bogus)) (:grid-template-rows (bogus))
(:grid-template-columns (0fr)) (:grid-template-columns (0fr))
(:grid-template-columns (0.0fr)) (:grid-template-columns (0.0fr))
(:grid-template-columns (1fr))
(:grid-template-columns (fr 1))
(:grid-template-columns min-content)
(:grid-template-columns ((fr 0))) (:grid-template-columns ((fr 0)))
(:grid-template-columns ((minmax (fr 1) auto))) (:grid-template-columns ((minmax (fr 1) auto)))
(:grid-template-columns (:grid-template-columns
((minmax (minmax (10) (20)) auto))) ((minmax (minmax (10) (20)) auto)))
(:grid-template-columns ((repeat 0 auto)))
(:grid-auto-columns ((repeat 2 auto)))
(:grid-auto-columns ((10) (20))) (:grid-auto-columns ((10) (20)))
(:row-gap -1) (:row-gap -1)
(:column-gap (-1)) (:column-gap (-1))
@ -1161,7 +1299,41 @@
(should-error (ebox-build form) :type 'error)) (should-error (ebox-build form) :type 'error))
(should-error (should-error
(ebox-build (ebox-test-box (ebox-test-text "raw runtime node"))) (ebox-build (ebox-test-box (ebox-test-text "raw runtime node")))
:type 'error)
(should-error
(ebox-build
(list 'box :color (tp-computed (lambda () "#112233")) "dynamic"))
:type 'error)
(should-error
(ebox-build
(list 'box :color
(tp-paint-slot-create '(:foreground "#112233")) "slot"))
:type 'error)) :type 'error))
(ert-deftest ebox-build-rejects-explicit-nil-outside-boolean-grammar ()
"Internal nil initial values must not widen the public author grammar."
(dolist (form
'((text :color nil "A")
(text :font-size nil "A")
(grid :grid-template-columns nil "A")
(grid :grid-template-rows nil "A")
(grid :grid-auto-columns nil "A")
(grid :grid-auto-rows nil "A")))
(should-error (ebox-build form) :type 'error))
(should (ebox-build '(box :border-top-p nil "A"))))
(ert-deftest ebox-source-metadata-uses-the-frozen-public-grammar ()
"Key, class, and id should remain source facts with closed value domains."
(should
(ebox-build
'(box :key stable :id 42 :class (card selected) "Valid")))
(dolist (form '((box :key nil "bad")
(box :key (not stable) "bad")
(box :id nil "bad")
(box :id 1.5 "bad")
(box :class (card 2) "bad")
(box :class (card . selected) "bad")))
(should-error (ebox-build form) :type 'error)))
(provide 'ebox-dsl-tests) (provide 'ebox-dsl-tests)
;;; ebox-dsl-tests.el ends here ;;; ebox-dsl-tests.el ends here

View File

@ -260,8 +260,8 @@
(should (= (apply #'+ widths) 150)) (should (= (apply #'+ widths) 150))
(should (> (car widths) (cadr widths))))) (should (> (car widths) (cadr widths)))))
(ert-deftest ebox-flex-basis-supports-content-and-intrinsic-keywords () (ert-deftest ebox-flex-basis-supports-intrinsic-keywords ()
"Flex basis content and intrinsic keywords should use content-derived sizes." "Flex basis intrinsic keywords should use content-derived sizes."
(let* ((content "aa bbbb") (let* ((content "aa bbbb")
(max-content (ebox--string-pixel-width content)) (max-content (ebox--string-pixel-width content))
(min-content (ebox--string-pixel-width "bbbb")) (min-content (ebox--string-pixel-width "bbbb"))
@ -269,7 +269,7 @@
(ebox-test-flex :width '(300) (ebox-test-flex :width '(300)
(ebox-test-flex-item (ebox-test-flex-item
(ebox-test-box (ebox-test-text content) :width '(160) :wrap-mode 'word) (ebox-test-box (ebox-test-text content) :width '(160) :wrap-mode 'word)
:flex-basis 'content) :flex-basis 'max-content)
(ebox-test-flex-item (ebox-test-flex-item
(ebox-test-box (ebox-test-text content) :width '(160) :wrap-mode 'word) (ebox-test-box (ebox-test-text content) :width '(160) :wrap-mode 'word)
:flex-basis 'min-content))) :flex-basis 'min-content)))
@ -733,8 +733,8 @@
(let* ((layout (let* ((layout
(ebox-test-flex :width '(120) (ebox-test-flex :width '(120)
(ebox-test-row (ebox-test-row
(ebox-test-box (ebox-test-text "Left") :border t) (ebox-test-box (ebox-test-text "Left") :border '(1 solid))
(ebox-test-box (ebox-test-text "Right") :border t)))) (ebox-test-box (ebox-test-text "Right") :border '(1 solid)))))
(widths (mapcar #'ebox--string-pixel-width (widths (mapcar #'ebox--string-pixel-width
(ebox-string-lines (ebox-render layout))))) (ebox-string-lines (ebox-render layout)))))
(should (cl-every (lambda (width) (<= width 120)) widths)))) (should (cl-every (lambda (width) (<= width 120)) widths))))
@ -1032,13 +1032,13 @@
:height 3 :height 3
:box-sizing 'border-box :box-sizing 'border-box
:padding '(0 (8)) :padding '(0 (8))
:border t :border '(1 solid)
:wrap-mode 'none) :wrap-mode 'none)
(ebox-test-box (ebox-test-text "sample box three") (ebox-test-box (ebox-test-text "sample box three")
:height 3 :height 3
:box-sizing 'border-box :box-sizing 'border-box
:padding '(0 (8)) :padding '(0 (8))
:border t :border '(1 solid)
:wrap-mode 'none))) :wrap-mode 'none)))
(plain (substring-no-properties (ebox-flex-test--render layout)))) (plain (substring-no-properties (ebox-flex-test--render layout))))
(should-not (string-match-p "REWRAPPED" plain)) (should-not (string-match-p "REWRAPPED" plain))
@ -1090,7 +1090,7 @@
(ebox-test-box (ebox-test-text "Bottom") :width '(80))) (ebox-test-box (ebox-test-text "Bottom") :width '(80)))
:width '(180) :width '(180)
:box-sizing 'border-box :box-sizing 'border-box
:border t))) :border '(1 solid))))
(plain (substring-no-properties (ebox-flex-test--render layout)))) (plain (substring-no-properties (ebox-flex-test--render layout))))
(should-not (string-match-p "REWRAPPED" plain)) (should-not (string-match-p "REWRAPPED" plain))
(should (string-match-p "Top" plain)) (should (string-match-p "Top" plain))
@ -1126,7 +1126,7 @@
"One child Box should own both frame and Flex participation facts." "One child Box should own both frame and Flex participation facts."
(let* ((left (ebox-test-box (ebox-test-text "A") :width '(60) (let* ((left (ebox-test-box (ebox-test-text "A") :width '(60)
:flex-grow 1 :flex-basis '(60) :flex-grow 1 :flex-basis '(60)
:box-sizing 'border-box :border t)) :box-sizing 'border-box :border '(1 solid)))
(right (ebox-test-box (ebox-test-text "B") :width '(40))) (right (ebox-test-box (ebox-test-text "B") :width '(40)))
(layout (ebox-test-flex :width '(160) left right)) (layout (ebox-test-flex :width '(160) left right))
(left-id (car (ebox-region-ids (ebox-test-root left)))) (left-id (car (ebox-region-ids (ebox-test-root left))))
@ -1176,20 +1176,22 @@
(ebox-test-box (ebox-test-text "A") :width '(60) :height 1) (ebox-test-box (ebox-test-text "A") :width '(60) :height 1)
(ebox-test-box (ebox-test-text "B") :width '(60) :height 1) (ebox-test-box (ebox-test-text "B") :width '(60) :height 1)
(ebox-test-box (ebox-test-text "C") :width '(60) :height 1)))))) (ebox-test-box (ebox-test-text "C") :width '(60) :height 1))))))
(dolist (justify '(flex-start flex-end center space-between space-around space-evenly start end left right normal)) (dolist (justify '(normal start end flex-start flex-end center stretch
space-between space-around space-evenly))
(should (stringp (should (stringp
(ebox-render (ebox-render
(ebox-test-flex :width '(220) :justify-content justify (ebox-test-flex :width '(220) :justify-content justify
(ebox-test-box (ebox-test-text "A") :width '(60)) (ebox-test-box (ebox-test-text "A") :width '(60))
(ebox-test-box (ebox-test-text "B") :width '(60))))))) (ebox-test-box (ebox-test-text "B") :width '(60)))))))
(dolist (align '(stretch flex-start flex-end center baseline normal start end self-start self-end)) (dolist (align '(normal start end stretch flex-start flex-end center baseline))
(should (stringp (should (stringp
(ebox-render (ebox-render
(ebox-test-flex :width '(220) :height 4 (ebox-test-flex :width '(220) :height 4
:align-items align :align-items align
(ebox-test-box (ebox-test-text "A") :width '(60) :height 1) (ebox-test-box (ebox-test-text "A") :width '(60) :height 1)
(ebox-test-box (ebox-test-text "B") :width '(60) :height 1)))))) (ebox-test-box (ebox-test-text "B") :width '(60) :height 1))))))
(dolist (align '(stretch flex-start flex-end center space-between space-around space-evenly baseline normal start end)) (dolist (align '(normal start end stretch flex-start flex-end center
space-between space-around space-evenly))
(should (stringp (should (stringp
(ebox-render (ebox-render
(ebox-test-flex :width '(130) :height 5 :flex-wrap 'wrap (ebox-test-flex :width '(130) :height 5 :flex-wrap 'wrap

152
tests/ebox-font-tests.el Normal file
View File

@ -0,0 +1,152 @@
;;; ebox-font-tests.el --- Font projection contracts -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'ebox)
(ert-deftest ebox-canonical-property-cutover-shares-one-resolved-font-fact ()
"Real Surface measurement and paint share one capability-resolved fact."
(let* ((ebox-font--cache (make-hash-table :test #'equal))
(resolver-calls 0)
(ebox-font--resolver
(lambda (face _window)
(cl-incf resolver-calls)
(list :identity (list 'resolved-font (copy-tree face))
:metrics (vector 'metrics (copy-tree face))))))
(let* ((below
(ebox-font--project
:size 1.39 :weight 'normal :style 'normal
:capability-signature 'display-a))
(above
(ebox-font--project
:size 1.4 :weight 'normal :style 'normal
:capability-signature 'display-a))
(same
(ebox-font--project
:size 1.4 :weight 'normal :style 'normal
:capability-signature 'display-a))
(changed-capability
(ebox-font--project
:size 1.4 :weight 'normal :style 'normal
:capability-signature 'display-b)))
(should (= (ebox-font-fact-requested-decipoints below) 10))
(should (= (ebox-font-fact-requested-decipoints above) 11))
(should (eq above same))
(should-not (eq above changed-capability))
(let ((identity (ebox-font-fact-resolved-font-identity above))
(metrics (ebox-font-fact-measurement-metrics above)))
(setf (plist-get identity :css-weight) 900)
(aset metrics 0 'damaged)
(should (= (plist-get
(ebox-font-fact-resolved-font-identity above)
:css-weight)
400))
(should (eq (aref (ebox-font-fact-measurement-metrics above) 0)
'metrics)))
(should (= resolver-calls 3)))
(setq resolver-calls 0
ebox-font--cache (make-hash-table :test #'equal))
(let ((capability 'display-a)
(buffer (generate-new-buffer " *ebox-font-contract*")))
(unwind-protect
(cl-letf (((symbol-function 'ebox--display-signature-for-window)
(lambda (_window) capability)))
(ebox-render-to-buffer
buffer
(ebox-build
'(box :width (viewport)
(text :font-size 16 :font-style italic "A"))))
(let* ((state-a (ebox--buffer-render-state buffer))
(node-a
(car (ebox-tree-node-children
(plist-get state-a :root-node))))
(fact-a (plist-get node-a :ebox-font-fact))
(measurement-fact nil)
(paint-fact nil)
(measurement-function
(symbol-function 'ebox-font-measurement-face))
(paint-function (symbol-function 'ebox-font-paint-face))
measured face)
(cl-letf (((symbol-function 'ebox-font-measurement-face)
(lambda (projection)
(setq measurement-fact projection)
(funcall measurement-function projection)))
((symbol-function 'ebox-font-paint-face)
(lambda (projection)
(setq paint-fact projection)
(funcall paint-function projection))))
(setq measured (ebox--propertize-typography "A" node-a)
face (ebox-buffer--font-face node-a)))
(should (ebox-font-fact-p fact-a))
(should (= resolver-calls 1))
(should (= (hash-table-count ebox-font--cache) 1))
(should (= (ebox-font-fact-requested-decipoints fact-a) 120))
(should (eq fact-a measurement-fact))
(should (eq fact-a paint-fact))
(should (integerp (plist-get face :height)))
(should (= (plist-get face :height) 120))
(should (eq (plist-get face :slant) 'italic))
(should (numberp (ebox-string-pixel-width measured)))
(setq capability 'display-b)
(let ((tp--surface-publication-step-function
(lambda (step _surface)
(when (eq step 'client-state)
(error "Reject font capability publication")))))
(should-error
(ebox-surface-update-buffer-viewport
buffer (plist-get state-a :viewport-width)
(plist-get state-a :viewport-height))))
(let* ((rolled-back (ebox--buffer-render-state buffer))
(rolled-back-node
(car (ebox-tree-node-children
(plist-get rolled-back :root-node)))))
(should (eq rolled-back state-a))
(should (eq (plist-get rolled-back-node :ebox-font-fact)
fact-a)))
(ebox-surface-update-buffer-viewport
buffer (plist-get state-a :viewport-width)
(plist-get state-a :viewport-height))
(let* ((state-b (ebox--buffer-render-state buffer))
(node-b
(car (ebox-tree-node-children
(plist-get state-b :root-node))))
(fact-b (plist-get node-b :ebox-font-fact)))
(should-not (eq fact-a fact-b))
(should (eq (ebox-font-fact-capability-signature fact-b)
'display-b))
(should (= resolver-calls 2))
(should (= (hash-table-count ebox-font--cache) 2)))))
(when (buffer-live-p buffer)
(kill-buffer buffer))))))
(ert-deftest ebox-relative-font-weight-resolves-before-emacs-paint ()
"CSS relative weight should not leak into an Emacs face attribute."
(let ((ebox-font--cache (make-hash-table :test #'equal))
(ebox-font--resolver
(lambda (face _window)
(list :identity (list :face face) :metrics nil)))
(buffer (generate-new-buffer " *ebox-font-relative*")))
(unwind-protect
(cl-letf (((symbol-function 'ebox--display-signature-for-window)
(lambda (_window) 'display-a)))
(ebox-render-to-buffer
buffer
(ebox-build
'(box :font-weight 400
(text :font-weight bolder "Bold"))))
(let* ((root (plist-get (ebox--buffer-render-state buffer)
:root-node))
(child (car (ebox-tree-node-children root)))
(fact (plist-get child :ebox-font-fact)))
(should (= (plist-get
(ebox-font-fact-resolved-font-identity fact)
:css-weight)
700))
(should (eq (plist-get (ebox-font-paint-face fact) :weight)
'bold))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(provide 'ebox-font-tests)
;;; ebox-font-tests.el ends here

View File

@ -30,17 +30,44 @@
"Fractional tracks should fill a definite grid width." "Fractional tracks should fill a definite grid width."
(let* ((node (ebox-test-grid (let* ((node (ebox-test-grid
:width '(120) :width '(120)
:grid-template-columns '(1fr 2fr) :grid-template-columns '((fr 1) (fr 2))
(ebox-test-box (ebox-test-text "A")) (ebox-test-box (ebox-test-text "A"))
(ebox-test-box (ebox-test-text "B")))) (ebox-test-box (ebox-test-text "B"))))
(lines (ebox-string-lines (ebox-render node)))) (lines (ebox-string-lines (ebox-render node))))
(should (= (length lines) 1)) (should (= (length lines) 1))
(should (= (ebox--string-pixel-width (car lines)) 120)))) (should (= (ebox--string-pixel-width (car lines)) 120))))
(ert-deftest ebox-grid-intrinsic-track-keywords-use-distinct-measures ()
"min-content and max-content should reuse Ebox intrinsic measurements."
(let* ((left (ebox-test-root (ebox-test-text "alpha beta")))
(right (ebox-test-root (ebox-test-text "gamma delta epsilon")))
(entries (list (list :node left :column 1 :column-span 1)
(list :node right :column 2 :column-span 1)))
(rendered (make-hash-table :test #'eq))
(tracks (ebox-grid--normalize-tracks
'(min-content max-content) 'columns)))
(puthash left "alpha beta" rendered)
(puthash right "gamma delta epsilon" rendered)
(let ((sizes (ebox-grid--resolve-sizes
tracks 2 'columns entries rendered 0 nil)))
(should
(= (car sizes) (ebox--line-min-content-pixel "alpha beta")))
(should
(= (cadr sizes) (ebox--string-pixel-width "gamma delta epsilon")))
(should (< (car sizes) (ebox--string-pixel-width "alpha beta"))))))
(ert-deftest ebox-grid-repeat-expands-a-complete-track-list ()
"repeat should duplicate its ordered track-list body."
(should
(equal
(ebox-grid--normalize-tracks '((repeat 2 (auto (fr 1)))) 'columns)
'((:kind auto :factor 0) (:kind fr :factor 1)
(:kind auto :factor 0) (:kind fr :factor 1)))))
(ert-deftest ebox-grid-repeated-render-reuses-construction-time-config () (ert-deftest ebox-grid-repeated-render-reuses-construction-time-config ()
"Grid render and resize paths should not reparse canonical track config." "Grid render and resize paths should not reparse canonical track config."
(let* ((node (ebox-test-grid :width '(120) (let* ((node (ebox-test-grid :width '(120)
:grid-template-columns '(1fr 2fr) :grid-template-columns '((fr 1) (fr 2))
(ebox-test-box (ebox-test-text "A")) (ebox-test-box (ebox-test-text "A"))
(ebox-test-box (ebox-test-text "B")))) (ebox-test-box (ebox-test-text "B"))))
(calls 0) (calls 0)
@ -102,8 +129,8 @@
:width '(120) :width '(120)
:grid-template-columns '((120)) :grid-template-columns '((120))
(ebox-test-row (ebox-test-row
(ebox-test-box (ebox-test-text "Left") :border t) (ebox-test-box (ebox-test-text "Left") :border '(1 solid))
(ebox-test-box (ebox-test-text "Right") :border t)))) (ebox-test-box (ebox-test-text "Right") :border '(1 solid)))))
(widths (mapcar #'ebox--string-pixel-width (widths (mapcar #'ebox--string-pixel-width
(ebox-string-lines (ebox-render node))))) (ebox-string-lines (ebox-render node)))))
(should (equal widths '(120))))) (should (equal widths '(120)))))
@ -148,7 +175,7 @@
"Grid wrapper borders should stay on one right edge across child lines." "Grid wrapper borders should stay on one right edge across child lines."
(let* ((node (ebox-test-grid (let* ((node (ebox-test-grid
:width '(120) :width '(120)
:grid-template-columns '((30) auto 1fr) :grid-template-columns '((30) auto (fr 1))
:grid-template-rows '(2) :grid-template-rows '(2)
:gap '(1 (4)) :gap '(1 (4))
:justify-content 'space-between :justify-content 'space-between
@ -165,7 +192,7 @@
(let* ((header (ebox-test-box (ebox-test-text "Header") :width '(720))) (let* ((header (ebox-test-box (ebox-test-text "Header") :width '(720)))
(grid (ebox-test-grid (grid (ebox-test-grid
:width '(718) :width '(718)
:grid-template-columns '((220) 1fr) :grid-template-columns '((220) (fr 1))
:gap '(1 (12)) :gap '(1 (12))
:border "#C97252" :border "#C97252"
(ebox-test-box (ebox-test-text "Fixed") :width '(196) (ebox-test-box (ebox-test-text "Fixed") :width '(196)

View File

@ -129,7 +129,7 @@
(should (commandp 'ebox-byte-compile)) (should (commandp 'ebox-byte-compile))
(should (should
(equal ebox--compile-sources (equal ebox--compile-sources
'("ebox-cache.el" "ebox-source.el" "ebox-style.el" '("ebox-cache.el" "ebox-source.el" "ebox-font.el" "ebox-style.el"
"ebox-layout-config.el" "ebox-layout-config.el"
"ebox-node-factory.el" "ebox-node-factory.el"
"ebox-child-range.el" "ebox-child-range.el"

View File

@ -177,7 +177,11 @@
(should pixelwise) (should pixelwise)
(if (eq window 'current-frame-window) 901 333))) (if (eq window 'current-frame-window) 901 333)))
((symbol-function 'window-body-height) ((symbol-function 'window-body-height)
(lambda (_window &optional _pixelwise) 31))) (lambda (_window &optional _pixelwise) 31))
((symbol-function 'ebox--display-signature-for-window)
(lambda (window)
(should (eq window 'current-frame-window))
'current-frame-capability)))
(let ((values (ebox-surface--context-values buffer nil nil))) (let ((values (ebox-surface--context-values buffer nil nil)))
(should (= 899 (plist-get values :viewport-width))) (should (= 899 (plist-get values :viewport-width)))
(should (= 31 (plist-get values :viewport-height))))) (should (= 31 (plist-get values :viewport-height)))))
@ -366,7 +370,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(cons 'grid (cons 'grid
(lambda () (lambda ()
(ebox-test-grid (ebox-test-grid
:width '(220) :grid-template-columns '((70) 1fr) :width '(220) :grid-template-columns '((70) (fr 1))
:grid-template-rows '(2) :gap '(1 (8)) :grid-template-rows '(2) :gap '(1 (8))
:border "#475569" :border "#475569"
(ebox-test-box :key 'grid-left (ebox-test-text "A\nAA") (ebox-test-box :key 'grid-left (ebox-test-text "A\nAA")
@ -602,9 +606,10 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(input (input
(ebox-test-box (ebox-test-box
:width 20 :width 20
(ebox-test-text (ebox-test-text "native")))
"native" :color 'ebox/default-foreground)))
(node (ebox-test-root input)) (node (ebox-test-root input))
(text-node (car (ebox-box-node-children node)))
(_ (plist-put text-node :color 'ebox/default-foreground))
(package (ebox-native-reflow--compile-layout-package node)) (package (ebox-native-reflow--compile-layout-package node))
(document (plist-get package :document)) (document (plist-get package :document))
(styles (plist-get document :styles)) (styles (plist-get document :styles))
@ -820,7 +825,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(ebox-surface-test--render-fresh (ebox-surface-test--render-fresh
(lambda () (lambda ()
(ebox-test-box (ebox-test-box
:font-height 1.25 :font-size 16
(ebox-test-box (ebox-test-text "Inherited")))) (ebox-test-box (ebox-test-text "Inherited"))))
t)) t))
(should (> calls 0)))) (should (> calls 0))))
@ -1253,7 +1258,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
collect collect
(ebox-test-flex-item (ebox-test-flex-item
(ebox-test-box (ebox-test-box
:key (list 'viewport-reflow-item index) :key (format "viewport-reflow-item-%d" index)
(ebox-test-text (format "item-%02d" index)) (ebox-test-text (format "item-%02d" index))
:width '(70) :width '(70)
:padding '(0 1) :padding '(0 1)
@ -1671,7 +1676,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(ebox-render-to-buffer (ebox-render-to-buffer
buffer buffer
(ebox-test-box (ebox-test-box
:font-height 1.25 :font-size 16
:width '(viewport) :width '(viewport)
(ebox-test-box (ebox-test-text "Inherited")))) (ebox-test-box (ebox-test-text "Inherited"))))
(cl-letf (((symbol-function 'ebox-surface--ensure-node-tree) (cl-letf (((symbol-function 'ebox-surface--ensure-node-tree)
@ -2014,8 +2019,8 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
ebox-surface--context-signals)) ebox-surface--context-signals))
(revision (tp-surface-revision surface)) (revision (tp-surface-revision surface))
(next-signature '(ebox-test-theme dark))) (next-signature '(ebox-test-theme dark)))
(cl-letf (((symbol-function 'ebox--display-signature) (cl-letf (((symbol-function 'ebox--display-signature-for-window)
(lambda () next-signature))) (lambda (_window) next-signature)))
(ebox-rerender-buffer-with-context buffer 120 4)) (ebox-rerender-buffer-with-context buffer 120 4))
(should (= (tp-surface-revision surface) (1+ revision))) (should (= (tp-surface-revision surface) (1+ revision)))
(should (equal (should (equal
@ -2034,10 +2039,12 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(ert-deftest ebox-inline-style-reuse-projects-onto-fresh-text-node () (ert-deftest ebox-inline-style-reuse-projects-onto-fresh-text-node ()
"A reused computed style must still materialize a fresh Text projection." "A reused computed style must still materialize a fresh Text projection."
(let* ((builder (ebox-source-builder-create)) (let* ((foreground "#0F172A")
(background "#F8FAFC")
(builder (ebox-source-builder-create))
(parent-declarations (parent-declarations
'(ebox/color foreground-slot (list 'ebox/color foreground
ebox/background-color background-slot)) 'ebox/background-color background))
(parent-handle (parent-handle
(ebox-source-builder-bind (ebox-source-builder-bind
builder :identity 'parent :declarations parent-declarations)) builder :identity 'parent :declarations parent-declarations))
@ -2098,12 +2105,12 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(ebox-surface-prepare-inline-candidate-styles (ebox-surface-prepare-inline-candidate-styles
old-state candidate '(1 2)) old-state candidate '(1 2))
(should (eq child-style (plist-get new-child :ebox-computed-style))) (should (eq child-style (plist-get new-child :ebox-computed-style)))
(should (eq 'foreground-slot (plist-get new-child :color))) (should (equal foreground (plist-get new-child :color)))
(should (equal "New" (ebox-text-node-value new-child))) (should (equal "New" (ebox-text-node-value new-child)))
(should (eq child-handle (ebox-node-source-handle new-child))) (should (eq child-handle (ebox-node-source-handle new-child)))
(dolist (property ebox-tree--participation-keys) (dolist (property ebox-tree--participation-keys)
(should-not (plist-member new-child property))) (should-not (plist-member new-child property)))
(should (eq 'background-slot (plist-get parent :bgcolor))))) (should (equal background (plist-get parent :bgcolor)))))
(ert-deftest ebox-style-theme-delta-reuses-cascade-facts () (ert-deftest ebox-style-theme-delta-reuses-cascade-facts ()
"Copy a paint-only Theme delta without rerunning ECSS cascade computation." "Copy a paint-only Theme delta without rerunning ECSS cascade computation."
@ -2177,7 +2184,7 @@ changes. Geometry and non-inherited computed values must remain identical."
(ecss-computed-style-values delta))) (ecss-computed-style-values delta)))
(let ((parent-font-new (let ((parent-font-new
(ebox-style-compute-subject (ebox-style-compute-subject
parent '(ebox/color "#222222" ebox/font-height 2.0)))) parent '(ebox/color "#222222" ebox/font-size 20))))
(should-not (should-not
(ebox-style--theme-parent-delta-computed (ebox-style--theme-parent-delta-computed
style declarations parent-old parent-font-new))))) style declarations parent-old parent-font-new)))))
@ -2194,16 +2201,16 @@ changes. Geometry and non-inherited computed values must remain identical."
(should-not (memq :ebox-content-width-exact-p (should-not (memq :ebox-content-width-exact-p
(ebox-tree-node-local-changed-keys old new))))) (ebox-tree-node-local-changed-keys old new)))))
(ert-deftest ebox-tree-grid-source-signature-canonicalizes-layout-aliases () (ert-deftest ebox-tree-grid-source-signature-canonicalizes-gap-shorthand ()
"Equivalent Grid gap/paint aliases must not become geometry dirtiness." "Equivalent Grid gap forms must not become geometry dirtiness."
(let ((old (list :ebox-type 'grid (let ((old (list :ebox-type 'grid
:raw-props '(:width stretch :raw-props '(:width stretch
:grid-template-columns (1fr 1fr) :grid-template-columns ((fr 1) (fr 1))
:grid-row-gap 1 :grid-column-gap (12) :row-gap 1 :column-gap (12)
:color "#252A2E" :background-color "#F8F5EE"))) :color "#252A2E" :background-color "#F8F5EE")))
(new (list :ebox-type 'grid (new (list :ebox-type 'grid
:raw-props '(:width stretch :raw-props '(:width stretch
:grid-template-columns (1fr 1fr) :grid-template-columns ((fr 1) (fr 1))
:gap (1 (12)) :gap (1 (12))
:color "#F2EEE4" :bgcolor "#1B1F20")))) :color "#F2EEE4" :bgcolor "#1B1F20"))))
(should-not (memq :props (ebox-tree-node-local-changed-keys old new))) (should-not (memq :props (ebox-tree-node-local-changed-keys old new)))
@ -2218,10 +2225,10 @@ changes. Geometry and non-inherited computed values must remain identical."
:padding-inline-end 2 :padding-inline-end 2
:padding-block-end 0 :padding-block-end 0
:padding-inline-start 2 :padding-inline-start 2
:border-top-width (1) :border-top-width 1
:border-right-width (1) :border-right-width 1
:border-bottom-width (1) :border-bottom-width 1
:border-left-width (1) :border-left-width 1
:border-top-style solid :border-top-style solid
:border-right-style solid :border-right-style solid
:border-bottom-style solid :border-bottom-style solid
@ -2237,7 +2244,7 @@ changes. Geometry and non-inherited computed values must remain identical."
:raw-props '(:width stretch :raw-props '(:width stretch
:gap (1 (12)) :gap (1 (12))
:padding (0 2) :padding (0 2)
:border ((1) solid "#687386") :border (1 solid "#687386")
:align-items center :align-items center
:color "#F2EEE4" :color "#F2EEE4"
:bgcolor "#1B1F20")))) :bgcolor "#1B1F20"))))
@ -2249,11 +2256,11 @@ changes. Geometry and non-inherited computed values must remain identical."
(let* ((parent (ecss-subject-create :type "box")) (let* ((parent (ecss-subject-create :type "box"))
(child (ecss-subject-create :type "box" :parent parent)) (child (ecss-subject-create :type "box" :parent parent))
(parent-old (parent-old
(ebox-style-compute-subject (ebox-style-compute-subject
parent '(ebox/font-height 1.0))) parent '(ebox/font-size 16)))
(parent-new (parent-new
(ebox-style-compute-subject (ebox-style-compute-subject
parent '(ebox/font-height 2.0))) parent '(ebox/font-size 20)))
(declarations '(ebox/color "#ffffff")) (declarations '(ebox/color "#ffffff"))
(style (style
(ebox-style-compute-subject child declarations parent-old))) (ebox-style-compute-subject child declarations parent-old)))