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
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

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_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
@ -25,11 +25,14 @@ compile:
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)'
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:
$(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:
$(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-build
'(column :padding (1 2)
:border ((1) solid "#8A93A6")
:border (1 solid "#8A93A6")
(text :color "#263244" "Hello Ebox")
(row :item-gap 1
(box "Left")

View File

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

View File

@ -90,7 +90,7 @@ Caller-owned Source Tree
## 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

View File

@ -90,7 +90,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
## 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 示例与测试

View File

@ -72,7 +72,9 @@ another public node or wrapper.
| Overflow | `:overflow` (`visible`, `hidden`, `scroll`) |
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
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` |
| Overflow | `:overflow``visible`、`hidden`、`scroll` |
普通横向数字表示字符列,单元素横向 list 表示像素;纵向数字表示行数。
`(viewport)``(viewport-height)` 使用当前 render context。property 验证属于
普通横向数字表示字符列,单元素横向 list 表示像素纵向数字表示行数。Border
width 是例外,始终使用非负整数像素,因此写 `(1 solid "#687386")`,不写
`((1) solid "#687386")`。`(viewport)` 与 `(viewport-height)` 使用当前 render context。property 验证属于
Eboxcontext/value 组合无效时会直接报错。
## 2. Evaluated typed construction

View File

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

View File

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

View File

@ -6,9 +6,24 @@
;;; 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 'subr-x)
(require 'ebox-measure)
(require 'ebox-font)
(require 'tp-style)
(defvar ebox--propertize-private-content-line-p nil
@ -320,29 +335,50 @@ Return FUNCTION's value."
;;; Paint Mapping
(defun ebox-buffer--font-attributes (style)
"Return anonymous face attributes for STYLE typography longhands."
(let (face)
(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-fact (style)
"Return STYLE's shared surface-resolved font fact."
(and (listp style) (plist-get style :ebox-font-fact)))
(defun ebox-buffer--font-face (style)
"Return STYLE's complete Emacs face value for typography."
(let* ((base (plist-get style :font))
(base (if (stringp base) (list :family base) base))
(attributes (ebox-buffer--font-attributes style)))
(cond
((and attributes base)
(if (and (listp base) (not (keywordp (car-safe base))))
(cons attributes base)
(list attributes base)))
(attributes attributes)
(base base))))
"Return a detached Emacs face from STYLE's shared font fact."
(when-let* ((fact (ebox-buffer--font-fact style)))
(ebox-font-paint-face fact)))
(defconst ebox-buffer--decoration-style-map
'((solid . line) (double . double-line) (dotted . dots)
(dashed . dashes) (wavy . wave))
"Canonical CSS decoration styles mapped to Emacs underline styles.")
(defun ebox-buffer--text-decoration-face (style)
"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)
"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
buffer-facing paint properties. Layout-only properties never pass through."
(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)))
(setq face
(cond
@ -397,7 +435,17 @@ buffer-facing paint properties. Layout-only properties never pass through."
(defun ebox--propertize-typography (string style)
"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)))
(ebox--add-render-face! copy 0 (length copy) face t)
(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)
node)
(defun ebox-canonical--text-measurement-property-p (property)
"Return non-nil when PROPERTY belongs to canonical Text measurement."
(eq (plist-get property :group) 'typography))
(defun ebox-canonical--text-engine-property-p (_property)
"Return non-nil for a Text fact materializable without a Surface.
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)
"Return non-nil when PROPERTY belongs to Box geometry or participation."
@ -284,20 +286,18 @@ and author TAG."
declarations
(lambda (property)
(or (funcall predicate property)
(memq (plist-get property :group)
'(paint typography)))))
(eq (plist-get property :group) 'paint))))
(ebox-style-declarations-require-projection-p
declarations
(lambda (property)
(or (funcall predicate property)
(memq (plist-get property :group)
'(paint typography))
(eq (plist-get property :group) 'paint)
(and (memq tag '(flex grid))
(memq tag
(plist-get property
:contexts)))))))))
(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)
facts))))
(let ((declarations
@ -316,13 +316,12 @@ and author TAG."
do (error "%s does not accept %S"
context (plist-get property :name)))
(list declarations
(ebox-style-expand-ebox-plist
(ebox-style--expand-engine-plist
(ebox-style-declaration-properties
declarations
(lambda (property)
(or (funcall predicate property)
(memq (plist-get property :group)
'(paint typography))))))
(eq (plist-get property :group) 'paint)))))
nil))))
(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."
(let ((predicate
(if (eq tag 'text)
#'ebox-canonical--text-measurement-property-p
#'ebox-canonical--text-engine-property-p
#'ebox-canonical--box-frame-property-p)))
(pcase-let ((`(,canonical ,engine ,style-required-p)
(ebox-canonical--node-properties

View File

@ -73,31 +73,49 @@
(when-let* ((entry (ebox-dsl--optional-field properties field context)))
(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)
"Compile and validate TAG author PROPERTIES for CONTEXT."
(condition-case err
(if properties
(ebox-style-compile-form tag properties)
nil)
(let ((declarations
(and properties (ebox-style-compile-form tag properties))))
(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 "%s rejected properties: %s"
context (error-message-string err)))))
(defun ebox-dsl--layout-config
(tag declarations extensions context)
"Construct TAG LayoutConfig from DECLARATIONS and EXTENSIONS for CONTEXT."
(defun ebox-dsl--layout-config (tag declarations context)
"Construct TAG LayoutConfig from canonical DECLARATIONS for CONTEXT."
(if (eq tag 'text)
nil
(condition-case err
(let ((names (ebox-layout-config-property-names tag)))
(ebox-layout-config-for-form
tag
(if (memq tag '(row column))
extensions
(let ((names (ebox-layout-config-property-names tag)))
(ebox-style-declaration-properties
declarations
(lambda (property)
(memq (plist-get property :name) names))))))
(memq (plist-get property :name) names)))))
(error
(error "%s rejected layout config: %s"
context (error-message-string err))))))
@ -114,6 +132,7 @@ function."
(let* ((context (format "Ebox %S" tag))
(properties
(ebox-dsl--validate-plist properties context)))
(ebox-dsl--validate-source-fields properties context)
(dolist (field '(:content :layout :display :ebox-type :ebox-content-node))
(when (plist-member properties field)
(error "%s does not accept private author property %S"
@ -124,17 +143,9 @@ function."
properties :outer context)))
(outer (and (not (eq tag 'text))
(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
(ebox-dsl--without-keys
properties
(append (unless (eq tag 'text) '(:outer))
extension-keys
ebox-dsl--source-fields)))
properties ebox-dsl--source-fields))
(declarations
(ebox-dsl--compile-style
tag style-properties context)))
@ -152,7 +163,7 @@ function."
:outer outer
:layout
(ebox-dsl--layout-config
tag declarations extensions context)
tag declarations context)
:declarations declarations
:owned-facts
(ebox-canonical-facts-from-declarations tag declarations)))))

View File

@ -102,18 +102,17 @@
"Supported canonical FlexConfig wrapping values.")
(defconst ebox--flex-justify-content-values
'(flex-start flex-end center space-between space-around space-evenly
start end left right normal)
'(normal start end flex-start flex-end center stretch
space-between space-around space-evenly)
"Supported canonical FlexConfig main-axis alignment values.")
(defconst ebox--flex-align-items-values
'(stretch flex-start flex-end center baseline normal
start end self-start self-end)
'(normal start end stretch flex-start flex-end center baseline)
"Supported canonical FlexConfig item alignment values.")
(defconst ebox--flex-align-content-values
'(stretch flex-start flex-end center space-between space-around space-evenly
baseline normal start end)
'(normal start end stretch flex-start flex-end center
space-between space-around space-evenly)
"Supported canonical FlexConfig line alignment values.")
(defconst ebox--flex-container-neutral-box-prop-keys
@ -1843,7 +1842,7 @@ a prefix of the eventual full wrapper render."
(unless complete
(plist-put copy :padding-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))
(defun ebox--flex-window-complete-prefix-supported-p (box)
@ -1858,7 +1857,7 @@ a prefix of the eventual full wrapper render."
line
(let ((region-id (ebox--ensure-region-id box)))
(puthash region-id box ebox--region-box-table)
(if (ebox-get box :border-bottom-p)
(if (> (ebox-get box :border-bottom-pixel) 0)
(propertize
(ebox--propertize-underline
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-color (ebox-get box :border-right-color))
(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))
(color (and (ebox-style--text-paint-owner-p box)
(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."
(let ((box (ebox--flex-window-box node)))
(if (and box
(ebox-get box :border-bottom-p)
(> (ebox-get box :border-bottom-pixel) 0)
(= (floor (ebox-get box :padding-bottom-height)) 0))
1
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))
(defconst ebox--paint-style-signature-keys
'(:color :bgcolor
:border-top-p :border-bottom-p
'(:color :bgcolor :visibility
:text-decoration-line :text-decoration-color :text-decoration-style
:border-left-color :border-right-color
:border-top-color :border-bottom-color)
"Box properties that affect paint without changing layout geometry.")
(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.")
(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
:margin-left-pixel :margin-right-pixel
: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
:overflow :wrap-mode
:flex-props :flex-participation))

View File

@ -18,6 +18,7 @@
(declare-function ebox--ensure-node-id "ebox" (node))
(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-node-factory--create-from-properties
"ebox-node-factory" (&rest plist))
@ -26,6 +27,10 @@
(declare-function ebox-string-lines "ebox" (string))
(declare-function ebox-string-height "ebox" (string))
(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--flex-spacing "ebox-flex" (mode leftover count base-gap))
(declare-function ebox--render-with-cache "ebox-incremental" (node &optional force cache-probe))
@ -34,7 +39,7 @@
(defconst ebox--grid-layout-prop-keys
'(:grid-template-columns :grid-template-rows
: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)
"Properties owned by the grid formatting context.")
@ -46,20 +51,20 @@
"Canonical property order stored in a typed GridConfig.")
(defconst ebox--grid-justify-items-values
'(stretch normal start end left right center)
'(stretch start end center)
"Supported canonical GridConfig inline item alignment 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.")
(defconst ebox--grid-content-alignment-values
'(stretch normal start end left right center
flex-start flex-end space-between space-around space-evenly)
'(normal start end stretch flex-start flex-end center
space-between space-around space-evenly)
"Supported canonical GridConfig content alignment values.")
(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.")
(defun ebox-grid--split-attrs (items)
@ -99,7 +104,17 @@
(unless (and (null (cdddr track))
(integerp count) (> count 0) value)
(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)))
tracks))
@ -110,14 +125,8 @@
value))
(cond
((eq value 'auto) '(:kind auto :factor 0))
((and (symbolp value)
(string-match "\\`\\([0-9]+\\(?:\\.[0-9]+\\)?\\)fr\\'"
(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)))
((memq value '(min-content max-content))
(list :kind value))
((and (consp value) (eq (car value) 'fr))
(unless (and (numberp (cadr value)) (> (cadr value) 0))
(error "ebox-grid: invalid fractional track %S" value))
@ -129,9 +138,11 @@
(error "ebox-grid: invalid minmax track %S" value))
(let ((minimum (ebox-grid--normalize-track (cadr 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))
(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))
(when (and (eq (plist-get maximum :kind) 'fixed)
(eq (plist-get minimum :kind) 'fixed)
@ -164,14 +175,10 @@
(let* ((pair (ebox--flex-gap-pair (plist-get props :gap)))
(row (cond
((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))))
(column (cond
((plist-member props :column-gap)
(plist-get props :column-gap))
((plist-member props :grid-column-gap)
(plist-get props :grid-column-gap))
(t (cdr pair)))))
(cons (or (ebox--flex-line-value row 0) 0)
(or (ebox--nonnegative-horizontal-size-pixels column 0) 0))))
@ -192,6 +199,13 @@
(defun ebox-grid--normalize-config-props (props)
"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)))
(list
:grid-template-columns
@ -221,11 +235,11 @@
ebox--grid-align-items-values :align-items)
:justify-content
(ebox-grid--enum-value
(plist-get props :justify-content) 'start
(plist-get props :justify-content) 'flex-start
ebox--grid-content-alignment-values :justify-content)
:align-content
(ebox-grid--enum-value
(plist-get props :align-content) 'start
(plist-get props :align-content) 'stretch
ebox--grid-content-alignment-values :align-content))))
(defconst ebox--grid-default-config-props
@ -238,6 +252,8 @@
(memq (plist-get track :kind) allowed-kinds)
(pcase (plist-get track :kind)
('auto (equal track '(:kind auto :factor 0)))
('min-content (equal track '(:kind min-content)))
('max-content (equal track '(:kind max-content)))
('fixed
(let ((size (plist-get track :size)))
(and (numberp size) (>= size 0)
@ -250,13 +266,15 @@
(defun ebox-grid--canonical-track-p (track)
"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)
(eq (plist-get track :kind) 'minmax)
(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
(plist-get track :max) '(fixed auto fr))
(plist-get track :max)
'(fixed auto min-content max-content fr))
(equal track
(list :kind 'minmax
:min (plist-get track :min)
@ -313,8 +331,8 @@
;;;###autoload
(defun ebox-grid-layout-create (&rest plist)
"Return a typed GridConfig from canonical evaluated PLIST.
Author `:gap' sugar and legacy grid-gap aliases are normalized before this
programmatic port and are therefore rejected here."
Author `:gap' sugar is expanded before this programmatic port. Removed
grid-gap aliases are rejected at the author schema boundary."
(ebox-grid--validate-config-input plist)
(let ((props (ebox-grid--normalize-config-props plist)))
(unless (ebox-grid-layout-config-props-p props)
@ -345,6 +363,11 @@ programmatic port and are therefore rejected here."
(if (> value 0)
(list value 1)
(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)))
(let ((start (car 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))
(column (ebox-grid--placement-part
(plist-get child :grid-column) 0))
(row-span (or (plist-get child :grid-row-span) (cadr row)))
(column-span (or (plist-get child :grid-column-span) (cadr column))))
(row-span (cadr row))
(column-span (cadr column)))
(dolist (part (list row column))
(when (or (not (integerp (car part))) (< (car part) 0)
(not (integerp (cadr part))) (< (cadr part) 1))
@ -491,6 +514,29 @@ programmatic port and are therefore rejected here."
(list (nreverse entries) matrix (length matrix)
(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)
"Return intrinsic size for TRACK from RENDERED grid ENTRIES."
(pcase (plist-get track :kind)
@ -500,6 +546,7 @@ programmatic port and are therefore rejected here."
(ebox-grid--track-intrinsic (plist-get track :min)
axis entries rendered key slot-index))
(_ (let ((position (1+ slot-index))
(mode (plist-get track :kind))
(best 0))
(dolist (entry entries best)
(when (and (<= (plist-get entry key) position)
@ -509,11 +556,9 @@ programmatic port and are therefore rejected here."
(if (eq key :column)
:column-span
:row-span)))))
(let* ((node (plist-get entry :node))
(string (gethash node rendered))
(value (if (eq axis 'columns)
(ebox--string-pixel-width string)
(ebox-string-height string))))
(let ((value
(ebox-grid--entry-intrinsic-size
entry rendered axis mode)))
(setq best (max best value)))))))))
(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)
"Return engine longhands changed between canonical OLD and NEW declarations."
(let ((all-properties (lambda (_property) t)))
(ebox-incremental--plist-changed-keys
(ebox-style-expand-ebox-plist
(ebox-style-declaration-properties old all-properties))
(ebox-style-expand-ebox-plist
(ebox-style-declaration-properties new all-properties)))))
(delete-dups
(cl-mapcan
#'ebox-style--engine-projection-targets
(ebox-incremental--plist-changed-keys old new))))
(defun ebox-incremental--candidate-source-record (source-index node)
"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)
(eq key :surface-properties)
(memq key ebox--paint-style-signature-keys)
(memq key '(:font :font-family :font-height
:font-weight :font-slant))))
(memq key '(:font-family :font-size
:font-weight :font-style))))
(plist-get entry :changed-keys))))
dirty-set)
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
:row-gap :column-gap
: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))))
(defun ebox-layout-config-for-form (tag properties)

View File

@ -58,8 +58,12 @@
"ebox-buffer-backend" (string property region-id))
(declare-function ebox--propertize-typography
"ebox-buffer-backend" (string style))
(declare-function ebox--propertize-text-decoration
"ebox-buffer-backend" (string style))
(declare-function ebox--propertize-underline
"ebox-buffer-backend" (string &optional color))
(declare-function ebox-buffer-validate-border-capability
"ebox-buffer-backend" (style))
(defvar ebox--render-runtime-revision nil
"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 :border-left-pixel) 0)
(equal (ebox-get box :border-right-pixel) 0)
(not (ebox-get box :border-top-p))
(not (ebox-get box :border-bottom-p))
(= (ebox-get box :border-top-pixel) 0)
(= (ebox-get box :border-bottom-pixel) 0)
(null (and (ebox-style--text-paint-owner-p box)
(ebox-get box :color)))
(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)
(= (ebox-get box :border-left-pixel) 0)
(= (ebox-get box :border-right-pixel) 0)
(not (ebox-get box :border-top-p))
(not (ebox-get box :border-bottom-p)))))
(= (ebox-get box :border-top-pixel) 0)
(= (ebox-get box :border-bottom-pixel) 0))))
(when (and lines-fit-p
(or (= text-height content-height)
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.
(if (eq (plist-get box :ebox-kind) 'box)
content
(ebox--propertize-typography content box))))
(ebox--propertize-text-decoration
(ebox--propertize-typography content box) box))))
(defun ebox--intrinsic-box-content (box)
"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-right-pixel (ebox-get box :border-right-pixel))
(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-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))
;; Style and Layout
@ -1887,7 +1892,9 @@ Internal implementation of `ebox-render' for box nodes."
(let* ((region-id (or ebox--render-region-id
(ebox--ensure-region-id node)))
(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
(if (or (ebox-get node :color) (ebox-get node :bgcolor))
(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
(make-hash-table :test 'eq))))
(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)))
(cond
((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-top-height :margin-bottom-height
:border-left-pixel :border-right-pixel))
(not (ebox-get node :border-top-p))
(not (ebox-get node :border-bottom-p))
(= (ebox-get node :border-top-pixel) 0)
(= (ebox-get node :border-bottom-pixel) 0)
(null (ebox-get node :color))
(null (ebox-get node :bgcolor))
(null (ebox-get node :font))
(null (ebox-get node :font-family))
(null (ebox-get node :font-height))
(null (ebox-get node :font-weight))
(null (ebox-get node :font-slant))
(or (null (plist-get node :ebox-font-fact))
(ebox-font-fact-neutral-p (plist-get node :ebox-font-fact)))
(null (ebox-get node :surface-properties))
(eq (ebox-get node :visibility) 'visible)
(eq (ebox-get node :text-align) 'left)
@ -2951,10 +2956,11 @@ no included vertical margin."
0))
(border-left-pixel (ebox-get box :border-left-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-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))
(color (and (ebox-style--text-paint-owner-p box)
(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-color (ebox-get box :border-right-color))
(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))
(color (and (ebox-style--text-paint-owner-p box)
(ebox-get box :color)))

View File

@ -8,6 +8,7 @@
(require 'cl-lib)
(require 'ebox-cache)
(require 'ebox-font)
(declare-function ebox--register-render-owned-text-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."
(ebox--string-pixel-width string))
(defun ebox--display-signature ()
"Return the current display signature for pixel measurement caches."
(list (frame-parameter nil 'font)
(frame-parameter nil 'font-backend)
(frame-parameter nil 'line-spacing)
(frame-parameter nil 'internal-border-width)
(face-attribute 'default :family nil t)
(face-attribute 'default :height nil t)
(face-attribute 'default :weight nil t)
(face-attribute 'default :slant nil t)
(face-attribute 'default :width nil t)
(face-attribute 'default :foreground nil t)
(face-attribute 'default :background nil t)
(defun ebox--display-signature-for-frame (frame)
"Return the pixel-measurement capability signature for FRAME."
(list frame
(frame-parameter frame 'font)
(frame-parameter frame 'font-backend)
(frame-parameter frame 'line-spacing)
(face-attribute 'default :family frame t)
(face-attribute 'default :height frame t)
(face-attribute 'default :weight frame t)
(face-attribute 'default :slant frame t)
(face-attribute 'default :width frame t)
(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 ()
"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."
(clrhash ebox--char-width-cache)
(ebox--clear-string-pixel-width-cache)
(ebox-font-clear-cache)
(setq ebox--space-pixel-cache nil)
(setq ebox--face-height-width-cache nil)
(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-tree-node-children "ebox-tree" (node))
(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--buffer-render-state "ebox-incremental" (buffer))
(declare-function ebox--ensure-region-id "ebox" (box))
@ -77,7 +79,11 @@
(declare-function ebox-pixel-space "ebox-measure" (pixel-width))
(declare-function ebox-string-lines "ebox" (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-paint-slot-p "tp-style" (value))
(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-top-height :margin-bottom-height
:border-left-pixel :border-right-pixel))
(not (ebox-get box :border-top-p))
(not (ebox-get box :border-bottom-p))
(= (ebox-get box :border-top-pixel) 0)
(= (ebox-get box :border-bottom-pixel) 0)
(null (ebox-get box :color))
(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)
"Compile canonical Text NODE to the lightweight native Text IR."
(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
(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
(ebox-native-reflow--compile-add-face-style
(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"
:region-id region-id
:content
(ebox-native-reflow--compile-text
(or (plist-get node :ebox-text-value)
(ebox-get node :content)
""))
(ebox-native-reflow--compile-text content)
:typography-style (or typography-style :null)
:foreground-style (or foreground-style :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)))
(border-left (floor (or (ebox-get box :border-left-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
(ebox-native-reflow--compile-add-face-style
(ebox-native-reflow--compile-foreground-face
@ -1808,7 +1826,8 @@ cross-axis stretch and item gap remain explicit without legacy runtime nodes."
(typography-style
(and (ebox-style--text-paint-owner-p box)
(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
(ebox-native-reflow--compile-add-face-style
(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
border-right (ebox-get box :border-right-color)))
(border-top-style
(and (ebox-get box :border-top-p)
(and (> border-top 0)
(ebox-native-reflow--compile-add-face-style
(let ((color (ebox-get box :border-top-color)))
(if (tp-paint-slot-p color)
(tp-paint-slot-face color)
(list :overline (or color t)))))))
(border-bottom-style
(and (ebox-get box :border-bottom-p)
(and (> border-bottom 0)
(ebox-native-reflow--compile-add-face-style
(let ((color (ebox-get box :border-bottom-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)
: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)
"Capture BUFFER's render-owned flat native scene without walking its tree."
(unless (buffer-live-p buffer)
@ -2238,7 +2263,7 @@ PROPERTY-TEMPLATES are Emacs-owned plists addressed by opaque native ids."
:node-postorder postorder
: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)))))
(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)
(eq (ebox--buffer-render-state buffer)
(ebox-native-reflow-layout-scene-render-state scene))
(with-current-buffer buffer
(equal (ebox-display-signature)
(ebox-native-reflow-layout-scene-display-signature
scene))))))
(equal (ebox-native-reflow--buffer-display-signature buffer)
(ebox-native-reflow-layout-scene-display-signature scene)))))
(defun ebox-native-reflow-layout-builder-step (builder 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
(make-hash-table :test 'equal))
(ebox-native-reflow--compile-display-signature
(ebox-display-signature))
(ebox--current-display-signature))
(ebox-native-reflow--compile-root-node node))
(cl-loop
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
(make-hash-table :test 'equal))
(ebox-native-reflow--compile-display-signature
(ebox-display-signature))
(ebox--current-display-signature))
(ebox-native-reflow--compile-root-node node)
(ebox--render-string-pixel-width-cache
(make-hash-table :test 'equal))
@ -4353,8 +4376,7 @@ creating and releasing a session for this preparation."
(or layout-package
(ebox-native-reflow--compile-layout-package root)))
(source-revision (or (plist-get state :runtime-revision) 0))
(display-signature
(with-current-buffer buffer (ebox-display-signature)))
(display-signature (plist-get state :display-signature))
(context-hash
(sxhash-equal
(list display-signature kind region-id
@ -4505,11 +4527,10 @@ When FULL-P is non-nil, query the complete restoration job."
(equal
(plist-get state :runtime-revision)
(ebox-native-reflow-preparation-expected-revision preparation))
(with-current-buffer buffer
(equal
(ebox-display-signature)
(ebox-native-reflow--buffer-display-signature buffer)
(ebox-native-reflow-preparation-display-signature
preparation))))
preparation)))
(error "Preview runtime changed during native reflow preparation"))
state))

View File

@ -18,18 +18,32 @@
:padding-top-height 0 :padding-bottom-height 0
:margin-left-pixel 0 :margin-right-pixel 0
:margin-top-height 0 :margin-bottom-height 0
:border-left-pixel 0 :border-left-color nil
:border-right-pixel 0 :border-right-color nil
:border-top-p nil :border-top-color nil
:border-bottom-p nil :border-bottom-color nil
:border-left-pixel 0 :border-left-style none :border-left-color nil
:border-right-pixel 0 :border-right-style none :border-right-color nil
:border-top-pixel 0 :border-top-style none :border-top-color nil
:border-bottom-pixel 0 :border-bottom-style none :border-bottom-color nil
:color nil :bgcolor nil
:font nil :font-family nil :font-height nil
:font-weight nil :font-slant nil
:text-decoration-line none :text-decoration-color currentColor
:text-decoration-style solid
:text-align left :vertical-align top
:overflow scroll :visibility visible
:wrap-mode word :scroll-offset 0)
"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
'(:content ""
:overflow visible
@ -44,7 +58,10 @@
:padding-top-height 0 :padding-bottom-height 0
:margin-left-pixel 0 :margin-right-pixel 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
:overflow scroll :visibility visible
: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)
"Create one private runtime box from evaluated engine PLIST."
(ebox-node-factory--create-expanded
(ebox-style-expand-ebox-plist plist)))
(ebox-style--expand-engine-plist plist)))
(provide 'ebox-node-factory)

View File

@ -216,6 +216,23 @@ Functions and records are opaque values and retain identity."
copy))
(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)
"Return VALUE normalized to a selector metadata string."
(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.
The returned handle contains identity only; all facts are owned by BUILDER
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
(ebox-source--make-handle
: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 'seq)
(require 'ebox-buffer-backend)
(require 'ebox-font)
(require 'ebox-style)
(require 'ebox-tree)
(require 'ebox-layout)
@ -49,6 +50,9 @@
(defvar ebox-surface--inline-style-value-cache nil
"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)
"Return non-nil when STATE's mixed proof contains one Range splice."
(cl-some
@ -633,7 +637,8 @@ and resize paths must consume this same choice."
:display-signature
(value :display-signature
(with-current-buffer buffer
(ebox--current-display-signature)))
(ebox--display-signature-for-window window)))
:display-window window
:scroll-offsets
(ebox-surface--scroll-offsets scroll-table)))))
@ -1025,6 +1030,8 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
success)
(unwind-protect
(progn
(let ((ebox-surface--font-window
(plist-get context-values :display-window)))
(tp-with-transaction
(ebox-surface--stage-signal-values signals context-values)
(if surface
@ -1041,7 +1048,7 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
(list :observers (list bridge)))))))
(ebox-surface--publish-runtime-state
buffer surface old-state report-base after-publication
framework-participant observation-context))
framework-participant observation-context)))
(setq success t)
(let ((scroll-keys
(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))
(old-height (plist-get state :viewport-height))
(target-height (or height old-height))
(window (ebox-surface--buffer-display-window buffer))
(display-signature
(with-current-buffer buffer
(ebox--current-display-signature)))
(ebox--display-signature-for-window window)))
(display-changed
(not (equal display-signature
(plist-get state :display-signature))))
@ -1859,6 +1867,8 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
(setq on-mismatch 'root))
(unwind-protect
(progn
(let ((ebox-surface--font-window
(plist-get context-values :display-window)))
(tp-with-transaction
(ebox-surface--stage-signal-values signals context-values)
(tp-surface-update-scoped
@ -1866,7 +1876,7 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
(list :on-mismatch on-mismatch :return-report nil))
(ebox-surface--publish-runtime-state
buffer surface old-state report-base after-publication
framework-participant observation-context))
framework-participant observation-context)))
(setq success t)
(let ((scroll-keys
(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
selector-tree-token)
(plist-put reused-state :subject subject)
(plist-put reused-state :node node)
(plist-put reused-state :parent-style-state
parent-style-state)
reused-state)
@ -2058,7 +2069,8 @@ published generation so a failed transaction has no cache state to restore."
0
(if parent-binding 1 0)))
:stylesheet-signature stylesheet-signature
:subject subject)))
:subject subject
:node node)))
(plist-put
new-state :compute
(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)
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)
"Return non-nil when NODE in SOURCE-INDEX needs closed inheritance."
(let ((declarations
@ -2260,6 +2304,14 @@ source-handle-to-subject table when the requested local preparation completed."
subject declarations
parent-style)))))
(when (and node style (not (eq node old-node)))
(let ((snapshot
(ebox-style--computed-snapshot style t)))
(ebox-surface--project-font
node style
(and parent-id
(plist-get (gethash parent-id new-nodes)
:ebox-font-fact))
snapshot)
(cond
((and source-stable-p
(ebox-style--computed-projection-transferable-p
@ -2269,9 +2321,10 @@ source-handle-to-subject table when the requested local preparation completed."
((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))
(ebox-style-apply-closed-inheritance
node style snapshot))
(t
(ebox-style-apply-computed node style)))
(ebox-style-apply-computed node style snapshot))))
(plist-put node :ebox-computed-style style)
(plist-put node :ebox-candidate-computed-style-p t))
(remhash node-id visiting)
@ -2303,6 +2356,16 @@ source-handle-to-subject table when the requested local preparation completed."
(when-let* ((subject
(ebox-source--subject-table-get
subject-table (ebox-tree-node-source-handle node))))
(let* ((parent-subject (ecss-subject-parent subject))
(parent-state (and parent-subject
(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)))
@ -2312,9 +2375,16 @@ source-handle-to-subject table when the requested local preparation completed."
(when-let* ((state (gethash parent states-by-subject)))
(puthash subject state states-by-subject))
(let ((style (tp-binding-read binding)))
(unless (and precomputed-p (equal style precomputed-style))
(let ((snapshot (ebox-style--computed-snapshot style t)))
(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))))
(let ((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
(ebox-surface--node-style-binding
source-index object node subject
@ -2322,12 +2392,15 @@ source-handle-to-subject table when the requested local preparation completed."
style-state-table
stylesheet-signature selector-tree-token
stylesheet-subject-local-p))))
(unless (and precomputed-p (equal style precomputed-style))
(let ((snapshot (ebox-style--computed-snapshot style t)))
(ebox-surface--project-font node style parent-font-fact snapshot)
(unless (equal style (plist-get node :ebox-computed-style))
(if (and (null (plist-get node :ebox-computed-style))
(ebox-surface--closed-canonical-node-p
source-index node))
(ebox-style-apply-closed-inheritance node style)
(ebox-style-apply-computed node style)))))))
(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)
"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 :border-left-pixel) 0) 0)
(= (or (ebox-get cached-box :border-right-pixel) 0) 0)
(not (ebox-get cached-box :border-top-p))
(not (ebox-get cached-box :border-bottom-p))
(= (or (ebox-get cached-box :border-top-pixel) 0) 0)
(= (or (ebox-get cached-box :border-bottom-pixel) 0) 0)
(eq (ebox-get cached-box :vertical-align) 'top)
(null (ebox-get cached-box :surface-properties))))
(visible
@ -4459,6 +4532,9 @@ NODE-OBJECTS and REGION-OBJECTS supply retained ownership ranges."
(when (ebox-style--text-paint-owner-p box)
(when-let* ((font (ebox-buffer--font-face box)))
(push font faces))
(when-let* ((decoration
(ebox-buffer--text-decoration-face box)))
(push decoration faces))
(when-let* ((color (plist-get box :color)))
(push (ebox-surface--foreground-face color) faces)))
(when-let* ((background (plist-get box :bgcolor)))
@ -4482,12 +4558,14 @@ ANCESTOR-P means BOX wraps a more specific rendered surface."
((or ancestor-p
(cl-intersection roles '(content content-owner pt pb pl pr)))
(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
(append faces
(list (list :overline
(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
(append
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))
(box (ebox-fragment-style-source-node node))
(region-id (plist-get box :region-id)))
(dolist (entry '((:border-top-p . bt) (:border-bottom-p . bb)))
(when (memq (car entry) (plist-get dirty :changed-keys))
(dolist (entry '(((:border-top-width :border-top-style)
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
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
(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
(plist-get previous-state :cascade-active-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
(ebox-surface--projection-start
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
(if (cdr axes) #'tp-signal-read #'tp-signal-peek)
(ebox-surface--signals-viewport-height signals))))
(display-signature
(and signals
(tp-signal-read
(ebox-surface--signals-display signals))))
(scroll-offsets
(and signals
(tp-signal-peek
@ -5055,7 +5155,7 @@ published runtime and must be consumed without clearing those shared nodes."
(ebox-surface--projection-state
root previous-state state-overrides projection-kind
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)
(when reuse-context-axes-p
(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
root previous-state state-overrides nil
stylesheet-active-p cascade-required-p axes
viewport-width viewport-height display-signature
viewport-width viewport-height
surface-display-signature
scroll-offsets))
(plist-put state :source-index source-index)
(plist-put state :style-binding-states

View File

@ -13,6 +13,7 @@
(require 'ecss-selector)
(require 'ebox-source)
(require 'ebox-style)
(require 'ebox-node-factory)
(require 'ebox-child-range)
(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.")
(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.")
(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
(append ebox-tree--runtime-source-keys
ebox-tree--child-source-keys
ebox-node-factory--style-projection-keys
'(:ebox-style-overrides
:ebox-source-handle
:ebox-grid-config
:ebox-computed-style :ebox-style-wrapper
:ebox-font-fact
:ebox-style-generated-wrapper
:ebox-author-style-count :ebox-author-style-required-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
;; snapshot and later from fresh shorthand declarations; comparing
;; 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)))
(cl-loop for (key value) on raw by #'cddr
unless (memq key '(:color :background-color :bgcolor :host-ref
@ -840,19 +843,9 @@ look like a geometry mutation."
(when (plist-member raw :gap)
(let* ((gap (plist-get raw :gap))
(row (if (consp gap) (car gap) gap))
(column (if (and (consp gap) (cdr gap)) (cadr gap) row))
;; Flex and Grid deliberately have separate canonical
;; property vocabularies. Using Grid aliases for a Flex node
;; 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)
(column (if (and (consp gap) (cdr gap)) (cadr gap) row)))
(puthash :row-gap row table)
(puthash :column-gap column table)
(remhash :gap table)))
(let (keys)
(maphash (lambda (key _value) (push key keys)) table)

172
ebox.el
View File

@ -23,7 +23,8 @@
"Directory containing the active Ebox Lisp 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-child-range.el" "ebox-tree.el" "ebox-measure.el"
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
@ -42,6 +43,7 @@
(require 'subr-x)
(require 'ebox-cache)
(require 'ebox-source)
(require 'ebox-font)
(require 'ebox-style)
(require 'ebox-layout-config)
(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."
(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
;;; ============================================================
@ -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 :border-left-pixel) 0) 0)
(= (or (ebox-get box :border-right-pixel) 0) 0)
(not (ebox-get box :border-top-p))
(not (ebox-get box :border-bottom-p))
(= (or (ebox-get box :border-top-pixel) 0) 0)
(= (or (ebox-get box :border-bottom-pixel) 0) 0)
(eq (ebox-get box :vertical-align) 'top)
(null (ebox-get box :surface-properties))
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 :border-left-pixel) 0) 0)
(= (or (ebox-get box :border-right-pixel) 0) 0)
(not (ebox-get box :border-top-p))
(not (ebox-get box :border-bottom-p))
(= (or (ebox-get box :border-top-pixel) 0) 0)
(= (or (ebox-get box :border-bottom-pixel) 0) 0)
(eq (ebox-get box :vertical-align) 'top)
(null (ebox-get box :surface-properties)))))
@ -3831,6 +3761,7 @@ Defaults to the current buffer."
(nreverse result)))
(defconst ebox--region-update-longhand-props
(append
'(:box-sizing
:width :min-width :max-width
:height :min-height :max-height
@ -3838,16 +3769,36 @@ Defaults to the current buffer."
:padding-top-height :padding-bottom-height
:margin-left-pixel :margin-right-pixel
:margin-top-height :margin-bottom-height
:border-left-pixel :border-left-color
:border-right-pixel :border-right-color
:border-top-p :border-top-color
:border-bottom-p :border-bottom-color
:border-left-pixel :border-left-style :border-left-color
:border-right-pixel :border-right-style :border-right-color
:border-top-pixel :border-top-style :border-top-color
:border-bottom-pixel :border-bottom-style :border-bottom-color
:color :bgcolor
:text-align :vertical-align
:overflow :wrap-mode
:scroll-offset :visibility)
ebox-style--item-projection-properties)
"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)
"Return the box longhand field for region-update PROPERTY."
(pcase property
@ -3884,6 +3835,8 @@ Defaults to the current buffer."
((memq target-key ebox--region-update-longhand-props)
(when (not (equal (ebox-get box target-key) value))
(setq style-changed-p t)))
((ebox-style-schema-id key)
(setq style-changed-p t))
(t
(push key unknown-keys))))
(when (and content-seen (not (stringp content-value)))
@ -3928,8 +3881,35 @@ cannot be changed through the unpublished batch root."
buffer region-id t))
(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
(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."
(let ((box (ebox--root-region-box root region-id))
(candidate-source-index source-index)
@ -3947,6 +3927,17 @@ cannot be changed through the unpublished batch root."
source-index old-handle :declarations declarations)))
(setq candidate-source-index (car 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
for target-key = (ebox--region-update-normalize-property key)
when (or (eq target-key :content)
@ -4195,12 +4186,23 @@ REGION-ID and CHANGED-KEYS describe one update; CHANGES describes a batch."
(candidate-box
(or (ebox--root-region-box published-root region-id)
(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
(ebox-style-merge-declarations
(ebox-style-node-declarations
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
(and (plist-member expanded :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
(ebox--surface-region-apply-props
candidate-root source-index
region-id expanded declarations))
region-id expanded declarations style-declarations))
(changed-keys (plist-get applied :changed-keys))
(candidate-source-index (plist-get applied :source-index))
(dirty-kind

View File

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

View File

@ -595,8 +595,8 @@
"Repeated equivalent style declarations compile through ECSS once."
(clrhash ebox-style--declaration-cache)
(let ((calls 0)
(original (symbol-function 'ecss-merge-declarations)))
(cl-letf (((symbol-function 'ecss-merge-declarations)
(original (symbol-function 'ecss-expand-declarations)))
(cl-letf (((symbol-function 'ecss-expand-declarations)
(lambda (&rest arguments)
(cl-incf calls)
(apply original arguments))))
@ -1038,57 +1038,6 @@
(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 ()
"Two variable-line owners should publish through their nearest stack context.
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")))
(old-root
(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 'right :source-identity 'right (ebox-test-text "right-old"))))
(buffer nil)
@ -1320,7 +1269,10 @@ remain retained identities."
'(span-patch owner-scoped)))
(should-not (plist-get report :tp-full-root))
(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"
(ebox-commit-test--buffer-string buffer)))
(setq fresh
@ -1328,7 +1280,7 @@ remain retained identities."
(generate-new-buffer-name
" *ebox-multi-owner-variable-slots-fresh*")
(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 'right :source-identity 'right (ebox-test-text "R")))))
(should (equal
@ -1347,7 +1299,7 @@ remain retained identities."
((root (left right)
(ebox-test-grid :key 'grid :width '(80)
: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 'right :source-identity 'right (ebox-test-text right))))
(candidate (buffer)
@ -1682,7 +1634,7 @@ remain retained identities."
:key 'geometry-context :width '(80)
(ebox-test-column
(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-text right))))
(ebox-test-box :key 'paint-a :source-identity 'paint-a
@ -1867,7 +1819,7 @@ remain retained identities."
(ebox-candidate-replace-host-ref
candidate '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
candidate 'right
(ebox-test-box :key 'right :source-identity 'right
@ -1991,9 +1943,23 @@ remain retained identities."
(defun ebox-commit-test--allocation-closure-root
(toast paint-a paint-b &optional width footer-overflow root-overflow)
"Return a generic whole-line Flex allocation-closure fixture."
(let* ((footer
(apply
#'ebox-test-flex
(append
(list :key 'footer :width (list (or width 800))
:flex-wrap 'wrap :gap '(1 (4)))
(when footer-overflow (list :overflow footer-overflow))
(list
(ebox-test-box
:key 'root :width (list (or width 800))
:height (and root-overflow 1) :overflow root-overflow
:key 'toast-slot :width 'stretch :min-width 0
:flex-grow 1 :flex-shrink 1 :flex-basis '(0)
(ebox-test-column
(ebox-test-box :key 'toast :source-identity 'toast
(ebox-test-text toast))))
(ebox-test-box :key 'peer
(ebox-test-text "database.sqlite"))))))
(content
(ebox-test-column
(ebox-test-box :key 'status :source-identity 'status
(ebox-test-text "Theme: Light") :width '(200))
@ -2003,16 +1969,13 @@ remain retained identities."
(ebox-test-text "paint-b") :bgcolor paint-b)
(ebox-test-box
:key 'footer-owner :width (list (or width 800))
(ebox-test-column
(ebox-test-flex
:key 'footer :width (list (or width 800)) :flex-wrap 'wrap
:overflow footer-overflow :gap '(1 (4))
(ebox-test-box
:key 'toast-slot :width 'stretch :min-width 0
:flex-grow 1 :flex-shrink 1 :flex-basis '(0)
(ebox-test-column
(ebox-test-box :key 'toast :source-identity 'toast (ebox-test-text toast))))
(ebox-test-box :key 'peer (ebox-test-text "database.sqlite"))))))))
(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
(buffer toast paint-a paint-b)

View File

@ -380,41 +380,37 @@
full-updates))
(defconst ebox-test--region-update-paint-cases
'((:color "#123456" "#123456")
(:bgcolor "#FDE68A" "#FDE68A")
(:border-left-color "#111111" "#111111")
(:border-right-color "#222222" "#222222")
(:border-top-color "#333333" "#333333")
(:border-bottom-color "#444444" "#444444")
(:border-top-p t t)
(:border-bottom-p t t))
"Paint-only box longhand update cases: (PROPERTY VALUE EXPECTED).")
'((:color "#123456" :color "#123456")
(:bgcolor "#FDE68A" :bgcolor "#FDE68A")
(:border-left-color "#111111" :border-left-color "#111111")
(:border-right-color "#222222" :border-right-color "#222222")
(:border-top-color "#333333" :border-top-color "#333333")
(:border-bottom-color "#444444" :border-bottom-color "#444444")
(:visibility hidden :visibility hidden))
"Paint updates as (AUTHOR VALUE ENGINE-FIELD EXPECTED).")
(defconst ebox-test--region-update-geometry-cases
'((:box-sizing content-box content-box owner-rerender)
(:width (120) 120 owner-rerender)
(:min-width (100) 100 owner-rerender)
(:max-width (140) 140 owner-rerender)
(:height 3 3 owner-rerender)
(:min-height 3 3 owner-rerender)
(:max-height 1 1 owner-rerender)
(:padding-left-pixel 9 9 span-patch)
(:padding-right-pixel 10 10 span-patch)
(:padding-top-height 1 1 span-patch)
(:padding-bottom-height 1 1 span-patch)
(:margin-left-pixel 7 7 owner-rerender)
(:margin-right-pixel 8 8 owner-rerender)
(:margin-top-height 1 1 owner-rerender)
(:margin-bottom-height 1 1 owner-rerender)
(:border-left-pixel 5 5 span-patch)
(:border-right-pixel 6 6 span-patch)
(:text-align center center span-patch)
(:vertical-align middle center span-patch)
(:overflow hidden hidden owner-rerender)
(:wrap-mode char char span-patch)
(:scroll-offset 1 1 span-patch)
(:visibility hidden hidden span-patch))
"Geometry box longhand update cases: (PROPERTY VALUE EXPECTED STRATEGY).")
'((:box-sizing content-box :box-sizing content-box owner-rerender)
(:width (120) :width 120 owner-rerender)
(:min-width (100) :min-width 100 owner-rerender)
(:max-width (140) :max-width 140 owner-rerender)
(:height 3 :height 3 owner-rerender)
(:min-height 3 :min-height 3 owner-rerender)
(:max-height 1 :max-height 1 owner-rerender)
(:padding-left (9) :padding-left-pixel 9 span-patch)
(:padding-right (10) :padding-right-pixel 10 span-patch)
(:padding-top 1 :padding-top-height 1 span-patch)
(:padding-bottom 1 :padding-bottom-height 1 span-patch)
(:margin-left (7) :margin-left-pixel 7 owner-rerender)
(:margin-right (8) :margin-right-pixel 8 owner-rerender)
(:margin-top 1 :margin-top-height 1 owner-rerender)
(:margin-bottom 1 :margin-bottom-height 1 owner-rerender)
(:border-top-p t :border-top-pixel 1 span-patch)
(:text-align center :text-align center span-patch)
(:overflow hidden :overflow hidden owner-rerender)
(:wrap-mode char :wrap-mode char span-patch)
(:scroll-offset 1 :scroll-offset 1 span-patch))
"Geometry updates as (AUTHOR VALUE ENGINE-FIELD EXPECTED STRATEGY).")
(defun ebox-test--region-update-case-properties (cases)
"Return the update property names covered by CASES."
@ -463,22 +459,15 @@
(should (ebox-test--face-has-key-value-p
face :background "#1E293B")))))
(ert-deftest ebox-ecss-value-resolver-uses-tp-computed-contract ()
"ECSS should resolve tagged TP values while preserving literal functions."
(let ((calls 0)
(literal (lambda () 'literal)))
(should
(= (plist-get
(ert-deftest ebox-font-size-rejects-runtime-and-function-values ()
"Standalone Ebox font size accepts only positive CSS reference pixels."
(should-error
(ebox-style-compute
(list :font-height
(tp-computed (lambda () (cl-incf calls) 1.25))))
:font-height)
1.25))
(should (= calls 1))
(should
(eq (plist-get (ebox-style-compute (list :font-height literal))
:font-height)
literal))))
(list :font-size (tp-computed (lambda () 16))))
:type 'error)
(should-error
(ebox-style-compute (list :font-size (lambda () 16)))
:type 'error))
(ert-deftest ebox-ecss-computed-fact-retains-owner-impact-and-provenance ()
"The Ebox package schema should remain attached to one computed fact."
@ -513,6 +502,11 @@
(paint-value (ecss-computed-property-fact-value fact)))
(should (equal '(geometry paint)
(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 (equal geometry-value
(ecss-computed-style-value style 'ebox/font-weight)))))
@ -551,7 +545,9 @@
(lambda (computed)
(cl-incf provenance-reads)
(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 (= active-reads 1))
(should (= specified-reads 1))
@ -590,31 +586,6 @@
(should (= active-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 ()
"Resolve GUI default foreground without inheriting its background."
(let ((original (symbol-function 'face-attribute)))
@ -700,7 +671,7 @@
(ebox-test--reset-runtime-state)
(let* ((root
(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-text "MMMM"))))
measured-face)
@ -716,18 +687,18 @@
(values (ecss-computed-style-values style))
(face (ebox-test--first-content-face (buffer-string))))
(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
(equal (ebox-test--effective-face-value face :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 measured-face)
(should
(equal (ebox-test--effective-face-value measured-face :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 ()
"A column background should fill its blank lines without hiding child paint."
@ -1781,6 +1752,31 @@
(should props)
(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 ()
"A TP paint slot should recolor retained output without property rewrites."
(let* ((slot (tp-paint-slot-create '(:foreground "red")))
@ -2120,7 +2116,7 @@
"Detailed snapshots should expose explicit L2 footprint signatures."
(ebox-test--reset-runtime-state)
(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))))
(ebox-test--with-rendered-buffer box
(let* ((snapshot (ebox--ensure-layout-snapshot-details
@ -2531,7 +2527,7 @@
collect
(ebox-test-box
(ebox-test-text (format "Fluid %02d" index))
:border t))))
:border '(1 solid)))))
buffer)
(unwind-protect
(progn
@ -2571,7 +2567,7 @@
collect
(ebox-test-box
(ebox-test-text (format "Fluid section %02d" index))
:border t))))
:border '(1 solid)))))
buffer)
(unwind-protect
(progn
@ -2611,7 +2607,7 @@
(ebox-test-text content)
:width 'viewport
:wrap-mode 'word
:border t))))
:border '(1 solid)))))
buffer)
(unwind-protect
(progn
@ -2645,7 +2641,7 @@
(ebox-test-text content)
:width 'viewport
:wrap-mode 'word
:border t))))
:border '(1 solid)))))
(slot-probe-count 0)
(original (symbol-function 'ebox-buffer--rendered-in-existing-slots))
buffer)
@ -3712,8 +3708,7 @@
(ordinary-height-nodes
(cl-loop for (property value)
in '((:height (viewport-height))
(:min-height (- (viewport-height) 1))
(:max-height (+ (viewport-height) 1)))
(:max-height (viewport-height)))
collect
(apply #'ebox-test-box
(ebox-test-text "Ordinary")
@ -3721,8 +3716,7 @@
(flex-height-nodes
(cl-loop for (property value)
in '((:height (viewport-height))
(:min-height (- (viewport-height) 1))
(:max-height (+ (viewport-height) 1)))
(:max-height (viewport-height)))
collect
(apply #'ebox-test-flex
(append
@ -4148,7 +4142,7 @@
"Render cache plist signatures should be built linearly."
(ebox-test--reset-runtime-state)
(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))
(append-calls 0))
(cl-letf (((symbol-function 'append)
@ -4234,9 +4228,9 @@
(ert-deftest ebox-transparent-preformatted-box-fast-path-is-exact ()
"Transparent exact-width wrappers should match the generic box pipeline."
(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))
(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))
(parent (ebox-test-column
:width '(viewport) :region-id 'parent child-a child-b))
@ -4343,11 +4337,12 @@
(ebox-test-box
(ebox-test-text (nth 0 case)) :width '(120) :height 5
: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))
(calls 0)
(original
(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)
(cl-letf (((symbol-function 'ebox--window-render-flat-wrapper-chunk-lines)
(lambda (&rest arguments)
@ -4756,12 +4751,12 @@
(should-not (text-property-search-forward 'ebox-bb left-id t))
(let ((full-rerenders
(ebox-test--count-full-surface-updates
(ebox-test--region-update left-id
:border-bottom-p t
:border-bottom-color "blue"))))
(ebox-test--region-update
left-id :border-bottom '(1 solid "blue")))))
(should (= full-rerenders 0)))
(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)))
(let (bottom-face)
(save-excursion
@ -4806,9 +4801,8 @@
(save-restriction
(goto-char (1+ (point-min)))
(narrow-to-region (point) (point-max))
(ebox-test--region-update left-id
:border-bottom-p t
:border-bottom-color "blue"))
(ebox-test--region-update
left-id :border-bottom '(1 solid "blue")))
(should (equal old-metadata
(plist-get old-state :surface-fragments)))
(should (cl-every (lambda (fragment)
@ -4818,9 +4812,9 @@
(ert-deftest ebox-horizontal-border-removal-preserves-other-border-colors ()
"Removing top/bottom border surfaces must not corrupt other border faces."
(ebox-test--reset-runtime-state)
(dolist (case '((:border-top-p bt (bb bl br))
(:border-bottom-p bb (bt bl br))))
(let* ((property (nth 0 case))
(dolist (case '(((:border-top-p nil) bt (bb bl br))
((:border-bottom-width 0) bb (bt bl br))))
(let* ((update (nth 0 case))
(removed-role (nth 1 case))
(kept-roles (nth 2 case))
(layout (ebox-test-box (ebox-test-text "Border")
@ -4841,34 +4835,46 @@
(setq face (get-text-property (point) 'face)))
(forward-char 1)))
face)))
(ebox-test--region-update region-id property nil)
(apply #'ebox-test--region-update region-id update)
(should-not (face-at-role removed-role))
(dolist (role kept-roles)
(should
(ebox-test--border-face-has-color-p
(face-at-role role) role "#D97757"))))))))
(ert-deftest ebox-side-border-color-patch-toggles-default-paint-exactly ()
"Side color patches should match default and explicit full-render paint."
(ert-deftest ebox-horizontal-border-capability-error-preserves-mounted-state ()
"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)
(let* ((layout
(ebox-test-box
(ebox-test-text "Border") :width 80
:border-left-pixel 2 :border-left-color "#AA0000"
:border-right-pixel 2 :border-right-color "#AA0000"))
:border-left-width 2 :border-left-style 'solid
:border-left-color "#AA0000"
:border-right-width 2 :border-right-style 'solid
:border-right-color "#AA0000"))
(region-id (car (ebox-region-ids 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
:border-left-color "#00AA00"
:border-right-color "#00AA00")
@ -4886,7 +4892,7 @@
"Runtime rerender should resolve viewport-sized boxes with the new context."
(ebox-test--reset-runtime-state)
(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)
(unwind-protect
(progn
@ -5510,7 +5516,8 @@
(let* ((layout
(ebox-test-box :id "root" :height 4 :box-sizing 'border-box :overflow
'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"
(ebox-test-column
(ebox-test-box :id "a" (ebox-test-text "A") :height 1)
@ -6175,7 +6182,7 @@
:text-align 'left))
(right (ebox-test-box (ebox-test-text "Right") :width '(80) :height 1))
(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--region-update left-id :text-align 'center)
(let ((report (ebox-test--buffer-update-report)))
@ -6189,6 +6196,51 @@
(buffer-substring-no-properties (point-min) (point-max))))
(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 ()
"Border-box left/right spacing can be internal when outer width is fixed."
(ebox-test--reset-runtime-state)
@ -6196,7 +6248,7 @@
:box-sizing 'border-box))
(region-id (car (ebox-region-ids 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)))
(should (eq (plist-get report :strategy) 'span-patch))
(should (equal (plist-get report :patch-ops) '(span-patch)))
@ -6209,7 +6261,7 @@
:box-sizing 'content-box))
(region-id (car (ebox-region-ids 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)))
(should (eq (plist-get report :strategy) 'owner-rerender))
(should (equal (plist-get report :patch-ops) '(owner-rerender)))
@ -6224,7 +6276,7 @@
:wrap-mode 'char))
(region-id (car (ebox-region-ids 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)))
(should (memq (plist-get report :strategy)
'(span-patch owner-rerender)))
@ -6233,27 +6285,35 @@
(should-not (plist-get report :tp-scope-fallback))
(should (ebox-test--buffer-propertized-matches-runtime-render-p))))))
(ert-deftest ebox-region-update-matrix-covers-all-box-longhands ()
"The update tests should classify every non-content box longhand."
(let ((covered (append (ebox-test--region-update-case-properties
(ert-deftest ebox-region-update-matrix-uses-only-public-properties ()
"Region update coverage must not preserve private engine spellings."
(dolist (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))))
(should-not (cl-set-difference ebox--region-update-longhand-props
covered
:test #'eq))
(should-not (cl-set-difference covered
ebox--region-update-longhand-props
:test #'eq))
(should-not (cl-set-difference ebox--paint-style-signature-keys
(ebox-test--region-update-case-properties
ebox-test--region-update-geometry-cases)))
(should (or (memq property ebox--region-update-control-properties)
(ebox-style-schema-id property))))
(dolist (property '(:padding-left-pixel :padding-right-pixel
:padding-top-height :padding-bottom-height
:margin-left-pixel :margin-right-pixel
:margin-top-height :margin-bottom-height
:border-left-pixel :border-right-pixel
:border-top-pixel :border-bottom-pixel
: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)
:test #'eq))))
(ebox-test--region-update-case-properties
ebox-test--region-update-geometry-cases))))))
(ert-deftest ebox-region-update-all-paint-longhands-use-paint-patch ()
"Every paint longhand should patch text properties without replacing spans."
(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))
(ebox-test--reset-runtime-state)
(let* ((box (ebox-test-box (ebox-test-text "Paint") :width '(120)
@ -6270,7 +6330,8 @@
(should (eq (gethash region-id ebox--region-box-table)
runtime-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)))
(should (eq (plist-get report :strategy) 'paint-patch))
(should (equal (plist-get report :patch-ops)
@ -6281,7 +6342,8 @@
(ert-deftest ebox-region-update-all-geometry-longhands-use-minimal-strategy ()
"Every geometry longhand should use a correct scoped non-root strategy."
(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))
(ebox-test--reset-runtime-state)
(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)
runtime-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)))
(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 (eq (plist-get report :owner-type) 'box))
(should-not (plist-get report :tp-full-root))
@ -6373,43 +6436,6 @@
(should (equal (plist-get report :dirty-kinds) '(paint)))
(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 ()
(ebox-test--reset-runtime-state)
(let* ((box (ebox-test-box (ebox-test-text "Box") :width 60))
@ -6417,7 +6443,7 @@
(ebox-test--with-rendered-buffer box
(ebox-test--region-update region-id
:padding '(1 2)
:border '(2 "#abcdef")
:border '(1 "#abcdef")
:margin-inline 3)
(let ((runtime-box
(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-right-pixel)
(* 2 (ebox--space-pixel-width))))
(should (= (ebox-get runtime-box :border-left-pixel)
(* 2 (ebox--space-pixel-width))))
(should (= (ebox-get runtime-box :border-left-pixel) 1))
(should (equal (ebox-get runtime-box :border-right-color) "#abcdef"))
(should (= (ebox-get runtime-box :margin-left-pixel)
(* 3 (ebox--space-pixel-width))))))))
@ -6461,8 +6486,7 @@
(should (= (ebox-get runtime-box :padding-top-height) 1))
(should (= (ebox-get runtime-box :padding-right-pixel)
(* 2 (ebox--space-pixel-width))))
(should (= (ebox-get runtime-box :border-left-pixel)
(ebox--space-pixel-width)))
(should (= (ebox-get runtime-box :border-left-pixel) 1))
(should (equal (ebox-get runtime-box :border-right-color) "#abcdef"))
(should (= (ebox-get runtime-box :margin-left-pixel)
(* 3 (ebox--space-pixel-width)))))
@ -6585,15 +6609,13 @@
(ebox-test--region-update region-id :color "#fedcba")
(ebox-test--assert-visible-overflow-paint
(buffer-string) "#fedcba" "#123456" region-id)
(let ((published (buffer-string)))
(should-error
(ebox-test--region-update region-id :color nil)
(cl-loop for line in (ebox-string-lines (buffer-string))
for glyph in '(?A ?B ?C)
for glyph-pos = (cl-position glyph line)
do (should (integerp glyph-pos))
do (should-not
(ebox-test--face-has-key-p
(get-text-property glyph-pos 'face line)
:foreground))))))
:type 'error)
(should (equal-including-properties published (buffer-string)))
(ebox-test--assert-visible-overflow-paint
(buffer-string) "#fedcba" "#123456" region-id)))))
(ert-deftest ebox-owner-rerender-keeps-visible-overflow-unowned ()
"Owner rerenders should not restore box ownership to visible overflow."
@ -6856,7 +6878,6 @@
:width 140
:height 5
:overflow 'scroll
:vertical-align 'top
:color "#112233"
:bgcolor "#ddeeff"))
(region-id (ebox--ensure-region-id (ebox-test-root box)))
@ -6864,7 +6885,6 @@
:width 140
:height 5
:overflow 'hidden
:vertical-align 'top
:color "#112233"
:bgcolor "#ddeeff"))
(expected (progn
@ -6937,7 +6957,7 @@
(ebox-test-box (ebox-test-text "") :width '(viewport)
:height 1)
(ebox-test-box :id "root" :width '(viewport) :height
'(- (viewport-height) 2) :overflow
'(viewport-height) :overflow
'scroll
(apply #'ebox-test-column
(append (list :width '(viewport) :padding '(1 (8)))
@ -6979,7 +6999,7 @@
:rendered-lines
(length (plist-get state
:rendered-content-lines)))))
(should (= (plist-get state :content-height) 12)))
(should (= (plist-get state :content-height) 14)))
(when-let* ((materialize
(plist-get state :materialize-content-lines)))
(plist-put
@ -7008,7 +7028,7 @@
(ebox-test--reset-runtime-state)
(let* ((layout
(ebox-test-box :id "root" :width '(viewport) :height
'(- (viewport-height) 2) :overflow 'scroll
'(viewport-height) :overflow 'scroll
(apply #'ebox-test-column
(append (list :width '(viewport))
(cl-loop for idx below 40 collect
@ -7026,7 +7046,7 @@
(ebox--intrinsic-layout-measurement t))
(setq rendered (ebox-render layout)))
(should (= calls 0))
(should (= (ebox-string-height rendered) 10))
(should (= (ebox-string-height rendered) 12))
(let ((ebox-viewport-width 240)
(ebox-viewport-height 12)
(ebox--intrinsic-layout-measurement nil))
@ -7038,7 +7058,7 @@
(ebox-test--reset-runtime-state)
(let* ((layout
(ebox-test-box :id "root" :width '(viewport) :height
'(- (viewport-height) 10) :overflow 'scroll
'(viewport-height) :overflow 'scroll
(apply #'ebox-test-column
(append (list :width '(viewport))
(cl-loop for idx below 80 collect
@ -7084,7 +7104,7 @@
(ebox-test--reset-runtime-state)
(let* ((layout
(ebox-test-box :id "root" :width '(viewport) :height
'(- (viewport-height) 10) :overflow 'scroll
'(viewport-height) :overflow 'scroll
(apply #'ebox-test-column
(append (list :width '(viewport))
(cl-loop for idx below 80 collect
@ -7148,7 +7168,7 @@
(ebox-test--reset-runtime-state)
(let* ((layout
(ebox-test-box :id "root" :width '(viewport) :height
'(- (viewport-height) 10) :overflow 'scroll
'(viewport-height) :overflow 'scroll
(apply #'ebox-test-column
(append (list :width '(viewport))
(cl-loop for idx below 80 collect
@ -7335,36 +7355,6 @@
(should (equal expected-signatures actual-signatures))
(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 ()
"A canonical Column should expose only the requested scroll prefix."
(ebox-test--reset-runtime-state)
@ -7809,27 +7799,29 @@
(= 1
(ebox--flex-window-completion-prefix-tail-rewrite-count
(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))))))
(should
(= 1
(ebox--flex-window-completion-prefix-tail-rewrite-count
(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
:margin-bottom 2))))))
(should
(= 0
(ebox--flex-window-completion-prefix-tail-rewrite-count
(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))))))
(should
(= 0
(ebox--flex-window-completion-prefix-tail-rewrite-count
(list :box (ebox-test-root
(ebox-test-box :border-bottom-p nil
:padding-bottom 0)))))))
(ebox-test-box :padding-bottom 0)))))))
(ert-deftest ebox-flex-window-cache-rejects-short-incomplete-exact-hit ()
"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 "height-only" :height
'(viewport-height))
(ebox-test-flex :id "flex-height" :min-height
'(- (viewport-height) 1)
(ebox-test-flex :id "flex-height" :height
'(viewport-height)
(ebox-test-box (ebox-test-text "Flex")))))))
(let* ((state (ebox--buffer-render-state buffer))
(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")
:width '(120)
:bgcolor "#111111"
:padding-left-pixel 2))
:padding-left 2))
(region-id (car (ebox-region-ids box))))
(ebox-test--with-rendered-buffer box
(should-error
(ebox-test--region-update region-id
:bgcolor "#222222"
:padding-left-pixel -1)
:padding-left -1)
:type 'error)
(should (equal (ebox-style-node-specified-value box :bgcolor)
"#111111"))
(should (= (ebox-style-node-specified-value
box :padding-left-pixel)
box :padding-left)
2)))))
(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")
:width 60
:padding '(1 1)
:border t)))
:border '(1 solid))))
(left-face (ebox-test--face-at-property rendered 'ebox-bl))
(right-face (ebox-test--face-at-property rendered 'ebox-br))
(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
:width (list 300))
(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")
(ebox-test-box (ebox-test-text "Footer line") :id 'footer
:width (list 300))))))
@ -13072,11 +13064,11 @@ face patch must match that exactly (issue014)."
(lambda (parent-color)
(ebox-test-box
:id 'parent :width (list 400)
:border (list '(4) 'solid parent-color)
:border (list 1 'solid parent-color)
:padding '(1 (12))
(ebox-test-column
(ebox-test-box :id 'child (ebox-test-text "CHILD") :width (list 200)
:border '((4) solid "#00AA00"))))))
:border '(1 solid "#00AA00"))))))
(visual-runs
(lambda (buffer role)
(with-current-buffer buffer

View File

@ -113,7 +113,7 @@
sum (ebox-dsl-test--tree-count child predicate)))))
(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
(list :bgcolor "#ffffff"
:padding '(1 2)
@ -124,7 +124,7 @@
(should (= (plist-get style :padding-inline-end) 2))
(should (= (plist-get style :padding-block-end) 1))
(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"))))
(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 :width) 'geometry))
(should (eq (ebox-style-dirty-kind :outer) 'structure))
(should (eq (ebox-style-dirty-kind :layout) 'structure))
(should (eq (ebox-style-dirty-kind :display) 'structure)))
(should-not (ebox-style-dirty-kind :layout))
(should-not (ebox-style-dirty-kind :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."
@ -143,6 +143,14 @@
(should (equal (ebox--computed-display (ebox-dsl-test--root node))
'(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 ()
"Unrelated computed facts must not replace direct typed layout values."
(dolist (case '((flex
@ -174,6 +182,80 @@
(equal (ebox-style-compile-declarations '(:bgcolor "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 ()
"One declaration source should not spell one property twice."
(should-error
@ -185,22 +267,24 @@
'(:background-color "red" :bgcolor "blue") t)
:type 'error))
(ert-deftest ebox-style-preserves-ordered-shorthand-longhand-normalization ()
"Ebox author pairs remain ordered normalization groups before cutover."
(let ((border-then-top
(ebox-style-compile-declarations
'(:border "red" :border-top none) t))
(top-then-border
(ebox-style-compile-declarations
'(:border-top none :border "red") t)))
(should (eq 'none
(plist-get border-then-top 'ebox/border-top-style)))
(should (equal "red"
(plist-get border-then-top 'ebox/border-right-color)))
(should (eq 'solid
(plist-get top-then-border 'ebox/border-top-style)))
(should (equal "red"
(plist-get top-then-border 'ebox/border-top-color)))))
(ert-deftest ebox-author-declaration-is-one-atomic-normalization-group ()
"One author group rejects overlaps while later cascade groups may override."
(dolist (declarations
'((:padding 1 :padding-left 2)
(:border "red" :border-top none)
(:gap 1 :row-gap 2)
(:flex-flow (row wrap) :flex-wrap nowrap)
(:flex (1 1 auto) :flex-grow 2)
(:border-top-p t :border-top-style none)))
(should-error
(ebox-style-compile-declarations declarations t)
:type 'ecss-invalid-declaration))
(should
(equal
(ebox-style-merge-declarations
(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 ()
"An alias should never shadow another canonical property."
@ -226,7 +310,7 @@
(setcar (plist-get metadata :contexts) 'damaged)
(should (eq :background-color
(plist-get (ebox-style-property :bgcolor) :name)))
(should (equal '(box)
(should (equal '(text box)
(plist-get (ebox-style-property :bgcolor) :contexts)))))
(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)))
(should (eq (ebox-style-canonical-name :border-top-p) :border-top-p)))
(ert-deftest ebox-style-exclusive-check-does-not-reexpand-ordinary-shorthands ()
"Conflict validation should leave the declaration cache effective."
(ert-deftest ebox-border-width-unit-is-always-integer-pixels ()
"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)
(original (symbol-function 'ebox-style--padding-shorthand)))
(clrhash ebox-style--declaration-cache)
@ -862,7 +987,7 @@
"The typed Grid constructor should emit one canonical closed property set."
(let* ((layout
(ebox-grid-layout-create
:grid-template-columns '((20) 1fr)
:grid-template-columns '((20) (fr 1))
:grid-template-rows '(1 auto)
:grid-auto-columns '((30))
:grid-auto-rows '(2)
@ -897,15 +1022,28 @@
(dolist (value ebox--grid-content-alignment-values)
(should (ebox-grid-layout-create :justify-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)
(:grid-template-columns (bogus))
(:grid-template-rows (bogus))
(:grid-template-columns (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 ((minmax (fr 1) auto)))
(:grid-template-columns
((minmax (minmax (10) (20)) auto)))
(:grid-template-columns ((repeat 0 auto)))
(:grid-auto-columns ((repeat 2 auto)))
(:grid-auto-columns ((10) (20)))
(:row-gap -1)
(:column-gap (-1))
@ -1161,7 +1299,41 @@
(should-error (ebox-build form) :type 'error))
(should-error
(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))
(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)
;;; ebox-dsl-tests.el ends here

View File

@ -260,8 +260,8 @@
(should (= (apply #'+ widths) 150))
(should (> (car widths) (cadr widths)))))
(ert-deftest ebox-flex-basis-supports-content-and-intrinsic-keywords ()
"Flex basis content and intrinsic keywords should use content-derived sizes."
(ert-deftest ebox-flex-basis-supports-intrinsic-keywords ()
"Flex basis intrinsic keywords should use content-derived sizes."
(let* ((content "aa bbbb")
(max-content (ebox--string-pixel-width content))
(min-content (ebox--string-pixel-width "bbbb"))
@ -269,7 +269,7 @@
(ebox-test-flex :width '(300)
(ebox-test-flex-item
(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-box (ebox-test-text content) :width '(160) :wrap-mode 'word)
:flex-basis 'min-content)))
@ -733,8 +733,8 @@
(let* ((layout
(ebox-test-flex :width '(120)
(ebox-test-row
(ebox-test-box (ebox-test-text "Left") :border t)
(ebox-test-box (ebox-test-text "Right") :border t))))
(ebox-test-box (ebox-test-text "Left") :border '(1 solid))
(ebox-test-box (ebox-test-text "Right") :border '(1 solid)))))
(widths (mapcar #'ebox--string-pixel-width
(ebox-string-lines (ebox-render layout)))))
(should (cl-every (lambda (width) (<= width 120)) widths))))
@ -1032,13 +1032,13 @@
:height 3
:box-sizing 'border-box
:padding '(0 (8))
:border t
:border '(1 solid)
:wrap-mode 'none)
(ebox-test-box (ebox-test-text "sample box three")
:height 3
:box-sizing 'border-box
:padding '(0 (8))
:border t
:border '(1 solid)
:wrap-mode 'none)))
(plain (substring-no-properties (ebox-flex-test--render layout))))
(should-not (string-match-p "REWRAPPED" plain))
@ -1090,7 +1090,7 @@
(ebox-test-box (ebox-test-text "Bottom") :width '(80)))
:width '(180)
:box-sizing 'border-box
:border t)))
:border '(1 solid))))
(plain (substring-no-properties (ebox-flex-test--render layout))))
(should-not (string-match-p "REWRAPPED" plain))
(should (string-match-p "Top" plain))
@ -1126,7 +1126,7 @@
"One child Box should own both frame and Flex participation facts."
(let* ((left (ebox-test-box (ebox-test-text "A") :width '(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)))
(layout (ebox-test-flex :width '(160) left right))
(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 "B") :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
(ebox-render
(ebox-test-flex :width '(220) :justify-content justify
(ebox-test-box (ebox-test-text "A") :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
(ebox-render
(ebox-test-flex :width '(220) :height 4
:align-items align
(ebox-test-box (ebox-test-text "A") :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
(ebox-render
(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."
(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 "B"))))
(lines (ebox-string-lines (ebox-render node))))
(should (= (length lines) 1))
(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 ()
"Grid render and resize paths should not reparse canonical track config."
(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 "B"))))
(calls 0)
@ -102,8 +129,8 @@
:width '(120)
:grid-template-columns '((120))
(ebox-test-row
(ebox-test-box (ebox-test-text "Left") :border t)
(ebox-test-box (ebox-test-text "Right") :border t))))
(ebox-test-box (ebox-test-text "Left") :border '(1 solid))
(ebox-test-box (ebox-test-text "Right") :border '(1 solid)))))
(widths (mapcar #'ebox--string-pixel-width
(ebox-string-lines (ebox-render node)))))
(should (equal widths '(120)))))
@ -148,7 +175,7 @@
"Grid wrapper borders should stay on one right edge across child lines."
(let* ((node (ebox-test-grid
:width '(120)
:grid-template-columns '((30) auto 1fr)
:grid-template-columns '((30) auto (fr 1))
:grid-template-rows '(2)
:gap '(1 (4))
:justify-content 'space-between
@ -165,7 +192,7 @@
(let* ((header (ebox-test-box (ebox-test-text "Header") :width '(720)))
(grid (ebox-test-grid
:width '(718)
:grid-template-columns '((220) 1fr)
:grid-template-columns '((220) (fr 1))
:gap '(1 (12))
:border "#C97252"
(ebox-test-box (ebox-test-text "Fixed") :width '(196)

View File

@ -129,7 +129,7 @@
(should (commandp 'ebox-byte-compile))
(should
(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-node-factory.el"
"ebox-child-range.el"

View File

@ -177,7 +177,11 @@
(should pixelwise)
(if (eq window 'current-frame-window) 901 333)))
((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)))
(should (= 899 (plist-get values :viewport-width)))
(should (= 31 (plist-get values :viewport-height)))))
@ -366,7 +370,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(cons 'grid
(lambda ()
(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))
:border "#475569"
(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
(ebox-test-box
:width 20
(ebox-test-text
"native" :color 'ebox/default-foreground)))
(ebox-test-text "native")))
(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))
(document (plist-get package :document))
(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
(lambda ()
(ebox-test-box
:font-height 1.25
:font-size 16
(ebox-test-box (ebox-test-text "Inherited"))))
t))
(should (> calls 0))))
@ -1253,7 +1258,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
collect
(ebox-test-flex-item
(ebox-test-box
:key (list 'viewport-reflow-item index)
:key (format "viewport-reflow-item-%d" index)
(ebox-test-text (format "item-%02d" index))
:width '(70)
:padding '(0 1)
@ -1671,7 +1676,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(ebox-render-to-buffer
buffer
(ebox-test-box
:font-height 1.25
:font-size 16
:width '(viewport)
(ebox-test-box (ebox-test-text "Inherited"))))
(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))
(revision (tp-surface-revision surface))
(next-signature '(ebox-test-theme dark)))
(cl-letf (((symbol-function 'ebox--display-signature)
(lambda () next-signature)))
(cl-letf (((symbol-function 'ebox--display-signature-for-window)
(lambda (_window) next-signature)))
(ebox-rerender-buffer-with-context buffer 120 4))
(should (= (tp-surface-revision surface) (1+ revision)))
(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 ()
"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
'(ebox/color foreground-slot
ebox/background-color background-slot))
(list 'ebox/color foreground
'ebox/background-color background))
(parent-handle
(ebox-source-builder-bind
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
old-state candidate '(1 2))
(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 (eq child-handle (ebox-node-source-handle new-child)))
(dolist (property ebox-tree--participation-keys)
(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 ()
"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)))
(let ((parent-font-new
(ebox-style-compute-subject
parent '(ebox/color "#222222" ebox/font-height 2.0))))
parent '(ebox/color "#222222" ebox/font-size 20))))
(should-not
(ebox-style--theme-parent-delta-computed
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
(ebox-tree-node-local-changed-keys old new)))))
(ert-deftest ebox-tree-grid-source-signature-canonicalizes-layout-aliases ()
"Equivalent Grid gap/paint aliases must not become geometry dirtiness."
(ert-deftest ebox-tree-grid-source-signature-canonicalizes-gap-shorthand ()
"Equivalent Grid gap forms must not become geometry dirtiness."
(let ((old (list :ebox-type 'grid
:raw-props '(:width stretch
:grid-template-columns (1fr 1fr)
:grid-row-gap 1 :grid-column-gap (12)
:grid-template-columns ((fr 1) (fr 1))
:row-gap 1 :column-gap (12)
:color "#252A2E" :background-color "#F8F5EE")))
(new (list :ebox-type 'grid
:raw-props '(:width stretch
:grid-template-columns (1fr 1fr)
:grid-template-columns ((fr 1) (fr 1))
:gap (1 (12))
:color "#F2EEE4" :bgcolor "#1B1F20"))))
(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-block-end 0
:padding-inline-start 2
:border-top-width (1)
:border-right-width (1)
:border-bottom-width (1)
:border-left-width (1)
:border-top-width 1
:border-right-width 1
:border-bottom-width 1
:border-left-width 1
:border-top-style solid
:border-right-style solid
:border-bottom-style solid
@ -2237,7 +2244,7 @@ changes. Geometry and non-inherited computed values must remain identical."
:raw-props '(:width stretch
:gap (1 (12))
:padding (0 2)
:border ((1) solid "#687386")
:border (1 solid "#687386")
:align-items center
:color "#F2EEE4"
:bgcolor "#1B1F20"))))
@ -2250,10 +2257,10 @@ changes. Geometry and non-inherited computed values must remain identical."
(child (ecss-subject-create :type "box" :parent parent))
(parent-old
(ebox-style-compute-subject
parent '(ebox/font-height 1.0)))
parent '(ebox/font-size 16)))
(parent-new
(ebox-style-compute-subject
parent '(ebox/font-height 2.0)))
parent '(ebox/font-size 20)))
(declarations '(ebox/color "#ffffff"))
(style
(ebox-style-compute-subject child declarations parent-old)))