refactor: restore the green typed layout baseline
This commit is contained in:
parent
8861902b7e
commit
d0ffe112e6
100
README.md
100
README.md
@ -1,69 +1,91 @@
|
|||||||
# Ebox
|
# Ebox
|
||||||
|
|
||||||
Ebox is a standalone, pixel-precise box and layout engine for Emacs. It provides the low-level rendering substrate used by the sibling [ETAF](../etaf/README.md) text-application framework. Ebox can also be used directly without Components, application state, or ETAF.
|
Ebox is a standalone Text/Box layout engine for Emacs. It owns text
|
||||||
|
measurement, box geometry, row/column/flex/Grid layout, retained rendering,
|
||||||
|
and incremental buffer publication. Use the sibling ETAF package when an
|
||||||
|
application also needs Components, reactive state, behaviors, or lifecycle.
|
||||||
|
|
||||||
## Install and use
|
## Install
|
||||||
|
|
||||||
ECSS and TP are independent packages and may be installed in either order. Install both before Ebox, or let a package manager resolve Ebox's two declared dependencies. For sibling source checkouts, place all three directories on `load-path`:
|
Ebox requires Emacs 29.1 or newer, ECSS, and TP. A package manager should
|
||||||
|
install the declared dependencies. For sibling source checkouts, add the three
|
||||||
|
directories to `load-path`, then load Ebox:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(add-to-list 'load-path "/path/to/ecss")
|
||||||
|
(add-to-list 'load-path "/path/to/tp")
|
||||||
|
(add-to-list 'load-path "/path/to/ebox")
|
||||||
|
(require 'ebox)
|
||||||
|
```
|
||||||
|
|
||||||
|
Loading Ebox does not create a buffer or build native code.
|
||||||
|
|
||||||
|
## First render
|
||||||
|
|
||||||
|
The ordinary author model has seven entries: a string, `text`, `box`, `row`,
|
||||||
|
`column`, `flex`, and `grid`. Children are nested directly; there is no second
|
||||||
|
field-based child syntax.
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(add-to-list 'load-path "/path/to/github/ecss")
|
|
||||||
(add-to-list 'load-path "/path/to/github/tp")
|
|
||||||
(add-to-list 'load-path "/path/to/github/ebox")
|
|
||||||
(require 'ebox)
|
(require 'ebox)
|
||||||
|
|
||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
"*Ebox Example*"
|
"*Ebox Example*"
|
||||||
(ebox-column
|
(ebox-build
|
||||||
(ebox-create :content "Hello Ebox"
|
'(column :padding (1 2)
|
||||||
:padding '(1 2)
|
:border ((1) solid "#8A93A6")
|
||||||
:border '((1) solid "#8A93A6")
|
(text :color "#263244" "Hello Ebox")
|
||||||
:color "#263244"
|
(row :item-gap 1
|
||||||
:bgcolor "#F4F6FB")))
|
(box "Left")
|
||||||
|
(box "Right")))))
|
||||||
```
|
```
|
||||||
|
|
||||||
The public model is a tree of Ebox nodes. `ebox-create` builds a leaf or wrapper node; `ebox-column`, `ebox-row`, `ebox-flex`, and `ebox-grid` compose nodes; `ebox-render` materializes propertized text without publishing buffer state (using isolated pure materialization when no cascade/inheritance dependency requires a TP object tree); `ebox-render-to-buffer` mounts a retained TP surface; `ebox-display-buffer` displays that same retained-surface path; and `ebox-commit` atomically updates the mount from a fresh root tree. Ebox copies declarative input before assigning runtime identity, so one source tree may be mounted in multiple buffers without transferring ownership. It exposes no separate public macro that erases a live buffer and evaluates arbitrary body forms.
|
Use `ebox-build` for the public author DSL. Framework integrations may instead
|
||||||
|
construct typed nodes with `ebox-text-create`, a typed LayoutConfig constructor,
|
||||||
|
and `ebox-box-create`; that evaluated API is not a second author grammar.
|
||||||
|
|
||||||
Framework integrations may wrap an entire input callback with `ebox-call-with-render-burst`, or pair `ebox-render-burst-begin` and `ebox-render-burst-end` under `unwind-protect`, so pre-commit view allocation and Ebox publication share one nested GC boundary.
|
## Layout choices
|
||||||
|
|
||||||
Material child lists may contain `ebox-child-range` descriptors, and logical candidates update their base-bound addresses with `ebox-candidate-replace-range-ref`. Candidates may also replace the private mounted root with `ebox-candidate-replace-root`; the last root replacement absorbs descendant operations and never shares an address with `:host-ref`. Reports expose `:range-metrics`, framework participant diagnostics, and scroll finalization diagnostics as read-only snapshots.
|
- `box` creates a normal visual box.
|
||||||
|
- `row` and `column` provide simple one-axis composition.
|
||||||
|
- `flex` distributes space and supports wrapping.
|
||||||
|
- `grid` provides two-dimensional tracks and placement.
|
||||||
|
- A bare string is the short form of `(text "...")`.
|
||||||
|
|
||||||
CSS-like selector strings are compiled to ECSS's structured selector AST. Ebox supplies logical node relations and indexed candidates; ECSS is the only selector matcher and cascade engine for tree queries, rendered-buffer queries, and style rules.
|
Flex and Grid participation properties belong directly to a child `box`. They
|
||||||
|
do not require a wrapper node.
|
||||||
|
|
||||||
## What belongs here
|
## Render and update
|
||||||
|
|
||||||
- box creation, padding, margins, borders, colors, faces, overflow, and text measurement;
|
- `ebox-render` returns propertized text without publishing a live buffer.
|
||||||
- row, column, flex, and two-dimensional Grid layout;
|
- `ebox-render-to-buffer` mounts a retained surface.
|
||||||
- selectors, stable node identity, keyed reconciliation, buffer rendering, and incremental publication;
|
- `ebox-commit` atomically publishes a newly built root.
|
||||||
- optional Rust native reflow with an exact Elisp fallback;
|
- `ebox-buffer-update-report` returns the last successful update report.
|
||||||
- low-level Ebox DSL files and renderer-focused tests.
|
- `ebox-rerender-buffer-with-context` applies an explicit viewport change.
|
||||||
|
|
||||||
ETAF belongs in the sibling package. Install it separately when you need the unified View grammar, Components, reactive state, behaviors, Context, Data Controllers, or application lifecycle. The independent example packages are [ebox-playground](../ebox-playground/README.md) and [etaf-playground](../etaf-playground/README.md).
|
Ebox copies author input before assigning runtime identity, so one built tree
|
||||||
|
may be mounted in multiple buffers without sharing live ownership.
|
||||||
|
|
||||||
## Repository map
|
## Optional native module
|
||||||
|
|
||||||
| Path | Responsibility |
|
The Rust module accelerates eligible reflow work. It is optional and has an
|
||||||
| --- | --- |
|
exact Elisp fallback. Ebox never builds it while loading.
|
||||||
| `ebox.el` | Public facade and package entry point. |
|
|
||||||
| `ebox-cache.el`, `ebox-style.el`, `ebox-tree.el`, `ebox-child-range.el`, `ebox-measure.el` | Cache, style, tree, persistent child-sequence, and measurement models. |
|
```elisp
|
||||||
| `ebox-fragment.el`, `ebox-render-context.el` | Layout facts and render-local context. |
|
(ebox-native-status)
|
||||||
| `ebox-layout.el`, `ebox-flex.el`, `ebox-grid.el` | Formatting contexts and layout algorithms. |
|
(ebox-native-build)
|
||||||
| `ebox-surface.el` | Ebox candidate projection plus TP surface mount/update and atomic Ebox runtime-state participation. |
|
```
|
||||||
| `ebox-buffer-backend.el`, `ebox-incremental.el` | Layout dirty planning, marker indexes, and specialized local update paths. |
|
|
||||||
| `ebox-dsl.el`, `ebox-selector.el` | Standalone DSL and ECSS-backed tree/runtime queries. |
|
|
||||||
| `ebox-native-reflow.el`, `native/` | Optional native reflow boundary. |
|
|
||||||
| `tests/` | Ebox-only regression and contract tests. |
|
|
||||||
| `docs/user/`, `docs/maintainer/` | Long-lived English and Chinese documentation. |
|
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
|
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make native-rust-tests
|
make native-rust-tests
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the smallest focused target first (`make grid-tests`, `make surface-tests`, `make dsl-tests`, or `make visual-check-tests`), then run `make check` for shared rendering or public API changes. Sibling ECSS and TP checkouts are found at `../new-architecture/ecss` and `../tp` by default; override `ECSS_DIR` or `TP_DIR` when they live elsewhere.
|
See the [user guide](docs/user/ebox-user-guide.en.md), the [public API
|
||||||
|
reference](docs/user/ebox-api-reference.en.md), and the sibling
|
||||||
Read the [Ebox user guide](docs/user/ebox-user-guide.en.md) for a guided start, the [public API reference](docs/user/ebox-api-reference.en.md) for the complete function/property/configuration inventory, and the [current implementation reference](docs/maintainer/ebox-current-implementation-reference.en.md) for ownership boundaries and invariants.
|
[ebox-playground](../ebox-playground/README.md) examples.
|
||||||
|
|||||||
@ -1,69 +1,87 @@
|
|||||||
# Ebox
|
# Ebox
|
||||||
|
|
||||||
Ebox 是一个独立的 Emacs 像素级 Box 与布局引擎。它是同级 [ETAF](../etaf/README.zh-CN.md) 文本应用框架使用的底层渲染基座;也可以脱离 Component、应用状态和 ETAF 单独使用。
|
Ebox 是一个独立的 Emacs Text/Box 布局引擎,负责文本测量、Box 几何、
|
||||||
|
row/column/flex/Grid 布局、retained 渲染和增量 buffer 发布。应用还需要
|
||||||
|
Component、响应式状态、behavior 或生命周期时,使用同级 ETAF 包。
|
||||||
|
|
||||||
## 安装与使用
|
## 安装
|
||||||
|
|
||||||
ECSS 与 TP 是互相独立的包,安装顺序任意。两者都安装后再安装 Ebox,或直接让包管理器解析 Ebox 声明的两个依赖。使用同级源码 checkout 时,把三个目录都加入 `load-path`:
|
Ebox 需要 Emacs 29.1 或更高版本,并依赖 ECSS 与 TP。包管理器应自动安装声明的
|
||||||
|
依赖。使用同级源码 checkout 时,把三个目录加入 `load-path` 后加载 Ebox:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(add-to-list 'load-path "/path/to/ecss")
|
||||||
|
(add-to-list 'load-path "/path/to/tp")
|
||||||
|
(add-to-list 'load-path "/path/to/ebox")
|
||||||
|
(require 'ebox)
|
||||||
|
```
|
||||||
|
|
||||||
|
加载 Ebox 不会创建 buffer,也不会构建 native 模块。
|
||||||
|
|
||||||
|
## 第一次渲染
|
||||||
|
|
||||||
|
普通用户只需理解七个 author 入口:字符串、`text`、`box`、`row`、`column`、
|
||||||
|
`flex` 和 `grid`。子节点直接嵌套,不存在第二套 field-based child 语法。
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(add-to-list 'load-path "/path/to/github/ecss")
|
|
||||||
(add-to-list 'load-path "/path/to/github/tp")
|
|
||||||
(add-to-list 'load-path "/path/to/github/ebox")
|
|
||||||
(require 'ebox)
|
(require 'ebox)
|
||||||
|
|
||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
"*Ebox Example*"
|
"*Ebox Example*"
|
||||||
(ebox-column
|
(ebox-build
|
||||||
(ebox-create :content "Hello Ebox"
|
'(column :padding (1 2)
|
||||||
:padding '(1 2)
|
:border ((1) solid "#8A93A6")
|
||||||
:border '((1) solid "#8A93A6")
|
(text :color "#263244" "Hello Ebox")
|
||||||
:color "#263244"
|
(row :item-gap 1
|
||||||
:bgcolor "#F4F6FB")))
|
(box "Left")
|
||||||
|
(box "Right")))))
|
||||||
```
|
```
|
||||||
|
|
||||||
公共模型是一棵 Ebox 节点树。`ebox-create` 构造叶子或包装节点;`ebox-column`、`ebox-row`、`ebox-flex` 和 `ebox-grid` 组合节点;`ebox-render` 生成带属性文本但不发布 buffer state(没有 cascade/inheritance 依赖时使用隔离的 pure materialization);`ebox-render-to-buffer` 挂载 retained TP surface;`ebox-display-buffer` 展示同一条 retained-surface 路径;`ebox-commit` 使用新的根树原子更新该 mount。Ebox 会在分配 runtime identity 前复制声明式输入,因此同一 source tree 可以挂载到多个 buffer,而不转移所有权。公共 API 不再提供另一套擦除 live buffer 后执行任意 BODY 的宏。
|
普通 author DSL 统一通过 `ebox-build`。框架集成可以直接组合
|
||||||
|
`ebox-text-create`、typed LayoutConfig constructor 和 `ebox-box-create`;这个
|
||||||
|
evaluated API 不是第二套 author 语法。
|
||||||
|
|
||||||
框架集成可以用 `ebox-call-with-render-burst` 包住整个输入 callback,或在 `unwind-protect` 中配对 `ebox-render-burst-begin` 与 `ebox-render-burst-end`,让 commit 前的 view 分配和 Ebox 发布共享同一个可嵌套 GC 边界。
|
## 如何选择布局
|
||||||
|
|
||||||
Material child list 可包含 `ebox-child-range` descriptor,logical candidate 用 `ebox-candidate-replace-range-ref` 更新其 base-bound 地址。Candidate 也可用 `ebox-candidate-replace-root` 替换私有 mounted root;最后一次 root replacement 会吸收 descendant operation,并且不会与 `:host-ref` 共用地址。报告通过 `:range-metrics`、framework participant diagnostics 和 scroll finalization diagnostics 暴露只读快照。
|
- `box` 创建普通视觉盒子;
|
||||||
|
- `row` 和 `column` 用于简单的一维组合;
|
||||||
|
- `flex` 用于空间分配和换行;
|
||||||
|
- `grid` 用于二维轨道与放置;
|
||||||
|
- 裸字符串是 `(text "...")` 的简写。
|
||||||
|
|
||||||
CSS-like selector 字符串会编译为 ECSS 的 structured selector AST。Ebox 提供逻辑节点关系与索引候选;对于 tree query、rendered-buffer query 和样式规则,ECSS 是唯一 selector matcher 与 cascade engine。
|
Flex/Grid participation property 直接属于子 `box`,不需要额外 wrapper 节点。
|
||||||
|
|
||||||
## 本包负责什么
|
## 渲染与更新
|
||||||
|
|
||||||
- box 创建、padding、margin、border、颜色、face、overflow 与文本测量;
|
- `ebox-render` 返回带属性文本,不发布 live buffer;
|
||||||
- row、column、flex 和二维 Grid 布局;
|
- `ebox-render-to-buffer` 挂载 retained surface;
|
||||||
- selector、稳定节点 identity、key reconciliation、buffer 渲染与增量发布;
|
- `ebox-commit` 原子发布重新构建的 root;
|
||||||
- 可选 Rust native reflow,以及完全等价的 Elisp fallback;
|
- `ebox-buffer-update-report` 返回最近一次成功更新报告;
|
||||||
- 底层 Ebox DSL 文件与渲染器测试。
|
- `ebox-rerender-buffer-with-context` 应用显式 viewport 变化。
|
||||||
|
|
||||||
ETAF 属于同级独立包。当你需要统一 View 语法、Component、响应式状态、behavior、Context、Data Controller 或应用生命周期时,单独安装 ETAF。两个独立的示例包是 [ebox-playground](../ebox-playground/README.md) 与 [etaf-playground](../etaf-playground/README.md)。
|
Ebox 会在分配 runtime identity 前复制 author 输入,因此同一棵已构建树可以挂载到
|
||||||
|
多个 buffer,而不会共享 live ownership。
|
||||||
|
|
||||||
## 仓库结构
|
## 可选 native 模块
|
||||||
|
|
||||||
| 路径 | 职责 |
|
Rust 模块只加速符合条件的 reflow;它不是正确性的依赖,并有完全等价的 Elisp
|
||||||
| --- | --- |
|
fallback。Ebox 加载时不会自动构建它。
|
||||||
| `ebox.el` | 公共门面与包入口。 |
|
|
||||||
| `ebox-cache.el`、`ebox-style.el`、`ebox-tree.el`、`ebox-child-range.el`、`ebox-measure.el` | 缓存、样式、树、持久 child sequence 和测量模型。 |
|
```elisp
|
||||||
| `ebox-fragment.el`、`ebox-render-context.el` | 布局事实与 render-local context。 |
|
(ebox-native-status)
|
||||||
| `ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` | Formatting Context 与布局算法。 |
|
(ebox-native-build)
|
||||||
| `ebox-surface.el` | 将 Ebox runtime/layout 投影为 TP surface plan,并负责 retained mount/update 与原子 runtime-state 参与。 |
|
```
|
||||||
| `ebox-buffer-backend.el`、`ebox-incremental.el` | Render-string backend 与 Ebox 增量规划。 |
|
|
||||||
| `ebox-dsl.el`、`ebox-selector.el` | 独立 DSL 与基于 ECSS 的树/runtime 查询。 |
|
|
||||||
| `ebox-native-reflow.el`、`native/` | 可选 native reflow 边界。 |
|
|
||||||
| `tests/` | 只属于 Ebox 的回归与契约测试。 |
|
|
||||||
| `docs/user/`、`docs/maintainer/` | 长期维护的中英文文档。 |
|
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
|
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make native-rust-tests
|
make native-rust-tests
|
||||||
```
|
```
|
||||||
|
|
||||||
小范围修改先运行对应目标(例如 `make grid-tests`、`make surface-tests`、`make dsl-tests` 或 `make visual-check-tests`);共享渲染或公共 API 修改后运行完整的 `make check`。默认从 `../new-architecture/ecss` 与 `../tp` 查找同级 ECSS 和 TP checkout;若路径不同可覆盖 `ECSS_DIR` 或 `TP_DIR`。
|
继续阅读[用户指南](docs/user/ebox-user-guide.zh.md)、[公共 API
|
||||||
|
参考](docs/user/ebox-api-reference.zh.md),以及同级
|
||||||
入门构造示例见 [Ebox 用户指南](docs/user/ebox-user-guide.zh.md),完整函数/property/配置清单见 [公共 API 参考](docs/user/ebox-api-reference.zh.md),职责边界与不变量见 [当前实现参考](docs/maintainer/ebox-current-implementation-reference.zh.md)。
|
[ebox-playground](../ebox-playground/README.md) 示例。
|
||||||
|
|||||||
@ -2,630 +2,224 @@
|
|||||||
|
|
||||||
[中文](ebox-api-reference.zh.md)
|
[中文](ebox-api-reference.zh.md)
|
||||||
|
|
||||||
This document is the exhaustive user-facing inventory for the standalone Ebox
|
This reference describes the current public boundary of the standalone Ebox
|
||||||
package. It describes the supported node model, properties, layout helpers,
|
package. It separates the ordinary author path from the evaluated typed API
|
||||||
rendering/publication workflows, selectors, scrolling, measurement, DSL, and
|
used by framework integrations. Names beginning with `ebox--` are private.
|
||||||
optional native module. `ebox--*` names and undocumented module helpers are
|
|
||||||
implementation details. The facade inventory is also available as the
|
|
||||||
`ebox-public-api` constant after `(require 'ebox)`.
|
|
||||||
|
|
||||||
Ebox owns box nodes, style normalization, measurement, layout, rendering,
|
Ebox requires Emacs 29.1 or newer, ECSS, and TP:
|
||||||
semantic update planning, selector integration, Grid, and the optional Rust
|
|
||||||
reflow accelerator. TP owns live buffer publication, physical diff execution,
|
|
||||||
revisions, and rollback. ECSS owns selector parsing, matching, and cascade
|
|
||||||
semantics. Ebox does not provide Components, application state, UI controls,
|
|
||||||
or an application lifecycle.
|
|
||||||
|
|
||||||
## 1. Install and load
|
|
||||||
|
|
||||||
Ebox requires Emacs 29.1 or newer, ECSS, and TP. With sibling source
|
|
||||||
checkouts:
|
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(add-to-list 'load-path "/path/to/github/ecss")
|
|
||||||
(add-to-list 'load-path "/path/to/github/tp")
|
|
||||||
(add-to-list 'load-path "/path/to/github/ebox")
|
|
||||||
(require 'ebox)
|
(require 'ebox)
|
||||||
```
|
```
|
||||||
|
|
||||||
Loading Ebox loads the Elisp layout/runtime modules. It does not create a
|
## 1. Author grammar
|
||||||
buffer, enable a mode in the current buffer, build Rust, or mutate the current
|
|
||||||
editing buffer. The optional native module is loaded only when its workflow or
|
|
||||||
an eligible native path requests it.
|
|
||||||
|
|
||||||
## 2. The normal lifecycle
|
`ebox-build` accepts exactly these author entries:
|
||||||
|
|
||||||
The public workflow is:
|
```text
|
||||||
|
STRING
|
||||||
```elisp
|
(text PROPERTY VALUE ... STRING)
|
||||||
(let ((root
|
(box PROPERTY VALUE ... CHILD ...)
|
||||||
(ebox-column
|
(row PROPERTY VALUE ... CHILD ...)
|
||||||
(ebox-create :id "status" :content "Ready" :width '(240))
|
(column PROPERTY VALUE ... CHILD ...)
|
||||||
(ebox-create :content "A caller-owned source tree"))))
|
(flex PROPERTY VALUE ... CHILD ...)
|
||||||
;; Pure materialization; returns a multi-line propertized string.
|
(grid PROPERTY VALUE ... CHILD ...)
|
||||||
(ebox-render root)
|
|
||||||
|
|
||||||
;; Retained TP surface; returns the live buffer.
|
|
||||||
(ebox-render-to-buffer "*Ebox Demo*" root)
|
|
||||||
|
|
||||||
;; Later, build a new declarative root and publish it atomically.
|
|
||||||
(ebox-commit
|
|
||||||
"*Ebox Demo*"
|
|
||||||
(ebox-column
|
|
||||||
(ebox-create :id "status" :content "Updated" :width '(240))
|
|
||||||
(ebox-create :content "The unchanged source is not mutated."))))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`ebox-render` has no live-buffer side effect. A static tree without an active
|
`STRING` is the short form of `(text STRING)`. A Text form requires exactly
|
||||||
stylesheet or inline inherited-style dependency can use isolated pure
|
one string payload. Every Box form accepts zero or more directly nested Text
|
||||||
materialization; stylesheet-dependent pure renders use an ephemeral TP
|
or Box children. The form name selects Normal, Row, Column, Flex, or Grid child
|
||||||
materialization. Both produce the same public string contract. The two buffer
|
layout; authors do not pass a layout selector as a property.
|
||||||
entry points use a retained TP surface. Ebox copies declarative input before
|
|
||||||
assigning runtime identity, so the same source tree may be mounted in several
|
|
||||||
buffers.
|
|
||||||
|
|
||||||
`ebox-display-buffer` is the convenience wrapper when Ebox should also display
|
|
||||||
the buffer. It deletes other windows before switching to the rendered buffer;
|
|
||||||
use `ebox-render-to-buffer` when the caller owns window selection.
|
|
||||||
|
|
||||||
## 3. Nodes and composition
|
|
||||||
|
|
||||||
### Box and simple layout constructors
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-create &rest properties)
|
|
||||||
(ebox-concat node-1 node-2) ; two nodes side by side
|
|
||||||
(ebox-stack node-1 node-2) ; two nodes vertically
|
|
||||||
(ebox-row &rest nodes) ; many nodes side by side; a sole Range keeps its row parent
|
|
||||||
(ebox-column &rest nodes) ; many nodes vertically; a sole Range keeps its column parent
|
|
||||||
(ebox-spacer &rest properties) ; blank box
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-create` returns a declarative box node. `:content` is string content;
|
|
||||||
container children are normally supplied to `ebox-row`, `ebox-column`,
|
|
||||||
`ebox-flex`, or `ebox-grid`. `ebox-concat` and `ebox-stack` accept two nodes;
|
|
||||||
use the variadic row/column helpers for more than two children. Nil children
|
|
||||||
are ignored. An empty row or column becomes a spacer.
|
|
||||||
|
|
||||||
In a `row`, omitted or `auto` widths use each child’s intrinsic width when the
|
|
||||||
children are concatenated; the parent viewport is not reused as every child’s
|
|
||||||
width. Explicit `viewport`, `stretch`, and definite widths still resolve against
|
|
||||||
their containing block. A `column` fills narrower layout rows to its containing
|
|
||||||
block, while a visual box paints its border and padding at its own used width.
|
|
||||||
|
|
||||||
`:key` is the stable application identity for a sibling. `:id` is a logical
|
|
||||||
selector/update identifier. `:host-ref` is an opaque application-owned anchor
|
|
||||||
used by host-reference lookup and logical candidate replacement. Host refs
|
|
||||||
must be unique and non-nil within one source tree.
|
|
||||||
|
|
||||||
### Flex
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-flex
|
|
||||||
:width '(480)
|
|
||||||
:flex-flow '(row wrap)
|
|
||||||
:gap '(1 (12))
|
|
||||||
(ebox-flex-item (ebox-create :content "A")
|
|
||||||
:flex '(1 1 auto)
|
|
||||||
:align-self 'center)
|
|
||||||
(ebox-create :content "B" :flex-grow 2))
|
|
||||||
```
|
|
||||||
|
|
||||||
Container properties are `:flex-direction`, `:flex-wrap`, `:flex-flow`,
|
|
||||||
`:justify-content`, `:align-items`, `:align-content`, `:gap`, `:row-gap`, and
|
|
||||||
`:column-gap`, plus ordinary box properties such as `:width`, `:height`,
|
|
||||||
padding, border, and paint. Defaults are row direction, nowrap, flex-start
|
|
||||||
main-axis justification, and stretch cross-axis alignment.
|
|
||||||
|
|
||||||
Item properties are `:order`, `:flex`, `:flex-grow`, `:flex-shrink`,
|
|
||||||
`:flex-basis`, and `:align-self`. They may be placed directly on a child or
|
|
||||||
attached with `ebox-flex-item`. Non-item properties passed to
|
|
||||||
`ebox-flex-item` wrap the child in a normal box.
|
|
||||||
|
|
||||||
The supported size keywords for flex preferred/min/max sizes are the Ebox
|
|
||||||
subset `auto`, `min-content`, `max-content`, `fit-content`, `stretch`, and
|
|
||||||
`contain`; the max-size set also accepts `none`. `fit-content` may carry a
|
|
||||||
numeric or viewport-relative limit. These are Ebox layout values, not a claim
|
|
||||||
of full browser CSS compatibility.
|
|
||||||
|
|
||||||
### Grid
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-grid
|
|
||||||
:width '(640)
|
|
||||||
:grid-template-columns '((200) 1fr 1fr)
|
|
||||||
:grid-template-rows '(1 1)
|
|
||||||
:gap '(1 (12))
|
|
||||||
(ebox-create :content "Header" :grid-column '(1 :span 3))
|
|
||||||
(ebox-create :content "Main" :grid-column 1 :grid-row 2)
|
|
||||||
(ebox-create :content "Aside" :grid-column 2 :grid-row 2))
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-grid-fr` constructs a fractional track: `(ebox-grid-fr 2)` returns
|
|
||||||
`(fr 2)`. Grid tracks support fixed sizes, `auto`, fractional tracks written
|
|
||||||
as `(fr FACTOR)` or symbols such as `1fr`, `minmax`, and `repeat`. Omitted
|
|
||||||
positions use implicit tracks. `:grid-auto-columns`, `:grid-auto-rows`, and
|
|
||||||
`:grid-auto-flow` (`row` or `column`) control implicit placement.
|
|
||||||
|
|
||||||
Grid item placement is one-based. `:grid-column` and `:grid-row` accept a
|
|
||||||
positive start integer, `(start :span positive-integer)`, or `(start end)` with
|
|
||||||
`end` greater than `start`. `:grid-column-span` and `:grid-row-span` accept
|
|
||||||
positive integers. Placement properties belong directly to the child Box;
|
|
||||||
Grid item is a parent-child role, not a wrapper or constructor.
|
|
||||||
|
|
||||||
## 4. Units, box properties, and text properties
|
|
||||||
|
|
||||||
### Size units
|
|
||||||
|
|
||||||
| Value | Meaning |
|
|
||||||
| --- | --- |
|
|
||||||
| Ordinary horizontal number, e.g. `12` | Character columns, converted using the current display's space width. |
|
|
||||||
| One-element horizontal list, e.g. `'(240)` | Absolute pixels. |
|
|
||||||
| Vertical number, e.g. `3` | Lines. |
|
|
||||||
| `(viewport)` or `viewport` | Current viewport width in pixels. |
|
|
||||||
| `(viewport-height)` or `viewport-height` | Current viewport height in lines. |
|
|
||||||
| `auto`, `min-content`, `max-content`, `fit-content`, `stretch`, `contain` | Intrinsic/preferred width values where the property and formatting context support them. |
|
|
||||||
|
|
||||||
`:box-sizing` defaults to `border-box`; `content-box` is also supported.
|
|
||||||
Width values include `:width`, `:min-width`, and `:max-width`; height values
|
|
||||||
include `:height`, `:min-height`, and `:max-height`. Negative geometry is
|
|
||||||
rejected. A viewport-dependent node must be rendered with a viewport context
|
|
||||||
or rerendered with `ebox-rerender-buffer-with-context`.
|
|
||||||
|
|
||||||
### Box, paint, and text properties
|
|
||||||
|
|
||||||
The following names are inputs to `ebox-create`. Style rules accept the
|
|
||||||
style/geometry names in this table, while `ebox-region-update` accepts the
|
|
||||||
mutable content/style/scroll subset supported by its target. Direct updates do
|
|
||||||
not add children or change selector metadata; use `ebox-commit` or a logical
|
|
||||||
candidate for structural changes. Structural metadata, `:content`, and
|
|
||||||
`:surface-properties` are node/render inputs rather than stylesheet
|
|
||||||
declarations. Canonical logical names and compatibility aliases are shown
|
|
||||||
together.
|
|
||||||
|
|
||||||
| Area | Properties |
|
|
||||||
| --- | --- |
|
|
||||||
| Content/geometry | `:content`, `:box-sizing`, `:width`, `:min-width`, `:max-width`, `:height`, `:min-height`, `:max-height` |
|
|
||||||
| Padding | `:padding`, `:padding-inline`, `:padding-block`; longhands `:padding-block-start`, `:padding-inline-end`, `:padding-block-end`, `:padding-inline-start`; aliases `:padding-top`, `:padding-right`, `:padding-bottom`, `:padding-left` and the pixel/height aliases `:padding-right-pixel`, `:padding-left-pixel`, `:padding-top-height`, `:padding-bottom-height` |
|
|
||||||
| Margin | `:margin`, `:margin-inline`, `:margin-block`; longhands `:margin-block-start`, `:margin-inline-end`, `:margin-block-end`, `:margin-inline-start`; aliases `:margin-top`, `:margin-right`, `:margin-bottom`, `:margin-left` and the pixel/height aliases `:margin-right-pixel`, `:margin-left-pixel`, `:margin-top-height`, `:margin-bottom-height` |
|
|
||||||
| Border | `:border`, `:border-top`, `:border-right`, `:border-bottom`, `:border-left`, `:border-width`, `:border-style`, `:border-color`; side longhands `:border-*-width`, `:border-*-style`, `:border-*-color`; Ebox boolean shorthands `:border-top-p`, `:border-bottom-p`; compatibility aliases `:border-left-pixel`, `:border-right-pixel` |
|
|
||||||
| Paint | `:color`, `:background-color`, and alias `:bgcolor` |
|
|
||||||
| Typography | `:font`, `:font-family`, `:font-height` and alias `:font-size`, `:font-weight`, `:font-slant` |
|
|
||||||
| Box content | `:text-align` (`left`, `center`, `right`), `:vertical-align` (`top`, `center`/`middle`, `bottom`), `:overflow` (`scroll`, `hidden`, `visible`), `:wrap-mode` (`word`, `char`, `kp`, or `none`; default `word`), `:visibility` (`visible` or `hidden`) |
|
|
||||||
| Structural style | `:display` is computed for node/layout contexts; the layout constructors normally choose the valid display tuple. |
|
|
||||||
| Flex container/item | `:flex-direction`, `:flex-wrap`, `:flex-flow`, `:justify-content`, `:align-items`, `:align-content`, `:gap`, `:row-gap`, `:column-gap` (aliases `:grid-row-gap`, `:grid-column-gap`); item names `:order`, `:flex`, `:flex-grow`, `:flex-shrink`, `:flex-basis`, `:align-self` |
|
|
||||||
| Grid container/item | `:grid-template-columns`, `:grid-template-rows`, `:grid-auto-columns`, `:grid-auto-rows`, `:grid-auto-flow`, `:justify-items`; item placement `:grid-column`, `:grid-row`, `:grid-column-span`, `:grid-row-span` |
|
|
||||||
| Extra text properties | `:surface-properties` accepts an even property list of Emacs text properties, applied to rendered characters without newlines. Inner non-nil properties win. |
|
|
||||||
|
|
||||||
Use `:font`, not `:face`, for the box font. `:font` accepts an Emacs face
|
|
||||||
symbol, a face name string, or a face plist; the typography longhands are
|
|
||||||
merged into that face. `:visibility 'hidden` suppresses ink while preserving
|
|
||||||
layout footprint and metadata. With `:overflow 'visible`, excess lines are
|
|
||||||
published as visible overflow; `hidden` clips them; `scroll` exposes a bounded
|
|
||||||
window and enables Ebox scrolling when content is taller than the box.
|
|
||||||
|
|
||||||
Padding and margin use CSS 1–4-value expansion. Horizontal entries use the
|
|
||||||
horizontal units above, while top/bottom entries use line units. Left/right
|
|
||||||
border widths are pixel geometry. Top/bottom borders use Emacs overline and
|
|
||||||
underline faces, so their width is a presence flag rather than arbitrary pixel
|
|
||||||
thickness.
|
|
||||||
|
|
||||||
### Node metadata and selector inputs
|
|
||||||
|
|
||||||
These inputs identify nodes without changing their geometry:
|
|
||||||
|
|
||||||
| Input | Meaning |
|
|
||||||
| --- | --- |
|
|
||||||
| `:id` | Logical selector/update id. Symbols and other scalar values are normalized to strings; an id should be unique where a direct update resolves it. |
|
|
||||||
| `:key` | Stable sibling identity used by reconciliation and also exposed as the built-in selector `:key` attribute. |
|
|
||||||
| `:class` | One class token or a list of class tokens for selector matching. |
|
|
||||||
| `:selector-state` | One state token or a list of state tokens matched by state pseudos such as `:hover` and `:active`. |
|
|
||||||
| `:selector-attributes` | Explicit attribute alist, such as `((role . button))`; content, layout values, and runtime fields are never inferred. |
|
|
||||||
| `:host-ref` | Unique application-owned opaque anchor for `ebox-host-ref-position`, `ebox-host-ref-bounds`, and host-ref candidate replacement. |
|
|
||||||
| `:scroll-offset` | Initial/controlled line offset for a scroll region. Prefer scroll commands for interaction; updates require a live region handle. |
|
|
||||||
|
|
||||||
`:id`, `:key`, and `:host-ref` are not stylesheet declarations. `:class`,
|
|
||||||
`:selector-state`, and `:selector-attributes` are selector metadata, not
|
|
||||||
general-purpose application state.
|
|
||||||
|
|
||||||
## 5. Stylesheets and cascade
|
|
||||||
|
|
||||||
Inline properties are enough for most nodes. For selector-driven styles, use
|
|
||||||
the isolated Ebox stylesheet:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-style-reset-rules)
|
|
||||||
(ebox-style-add-rule
|
|
||||||
".card"
|
|
||||||
'(:color "#1F2937"
|
|
||||||
:background-color "#F8FAFC"
|
|
||||||
:padding '(1 (12)))
|
|
||||||
:layer 'base)
|
|
||||||
(ebox-style-add-rule
|
|
||||||
".card:has(> .warning)"
|
|
||||||
'(:border-color "#DC2626")
|
|
||||||
:layer 'state)
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-style-add-rule` accepts a selector string or parsed AST, an Ebox
|
|
||||||
declaration plist, and the ECSS cascade keywords `:origin`, `:layer`, and
|
|
||||||
`:scope`. `ebox-style-reset-rules` clears the stylesheet. Rules are global to
|
|
||||||
the loaded Ebox style registry; changing rules does not silently publish an
|
|
||||||
already-mounted buffer, so rerender or commit the affected buffer after a
|
|
||||||
rule change. `ebox-style-cascade-active-p` reports whether rules are present.
|
|
||||||
|
|
||||||
The ECSS-backed style domain includes inheritance for color and typography,
|
|
||||||
computed values, cascade layers, custom-property declarations, and dirty
|
|
||||||
classification for paint/geometry/structure changes. Ebox interprets only the
|
|
||||||
properties listed in this reference; percentages, absolute positioning,
|
|
||||||
z-index, shadows, border radius, browser bidi, and full browser typography are
|
|
||||||
outside the package.
|
|
||||||
|
|
||||||
Custom properties whose names start with `--` may be carried by the ECSS style
|
|
||||||
domain, but they do not affect Ebox output unless a host integration resolves
|
|
||||||
them into one of the supported Ebox properties.
|
|
||||||
|
|
||||||
## 6. Rendering, identity, and updates
|
|
||||||
|
|
||||||
### Region ids and logical handles
|
|
||||||
|
|
||||||
`ebox-region-ids` returns region ids in document order from an unrendered tree
|
|
||||||
or rendered string. Capture ids before the first mount if a low-level source
|
|
||||||
mapping needs them:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(let* ((root (ebox-column
|
|
||||||
(ebox-create :id "title" :content "Title")
|
|
||||||
(ebox-create :id "body" :content "Body")))
|
|
||||||
(ids (ebox-region-ids root)))
|
|
||||||
(ebox-render-to-buffer "*Ebox Demo*" root)
|
|
||||||
ids)
|
|
||||||
```
|
|
||||||
|
|
||||||
For application updates, prefer logical ids and opaque handles:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(let ((handle (ebox-region-resolve "*Ebox Demo*" "body")))
|
|
||||||
(ebox-region-update handle :content "Changed" :color "#166534"))
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-region-resolve` signals when the buffer is absent, the logical id is
|
|
||||||
missing, or the id is ambiguous. A handle is scoped to one retained TP
|
|
||||||
surface; it becomes stale when its object is removed or its buffer is killed.
|
|
||||||
Numeric region ids are render metadata and are not accepted by
|
|
||||||
`ebox-region-update`.
|
|
||||||
|
|
||||||
### Direct and selector updates
|
|
||||||
|
|
||||||
`ebox-region-update` accepts mutable content/style/scroll property keywords and
|
|
||||||
returns the committed update report unless an explicit batch queues it. It
|
|
||||||
builds an isolated candidate, plans the smallest safe Ebox owner, and asks TP
|
|
||||||
to publish once. It does not add children or mutate `:id`, `:class`, `:key`,
|
|
||||||
or other selector metadata. Invalid declarations, render failures, publication
|
|
||||||
failures, and callback failures leave the previous buffer/runtime/report intact.
|
|
||||||
|
|
||||||
`ebox-selector-update-buffer` applies properties to all editable selector
|
|
||||||
matches and returns:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(:selector SELECTOR
|
|
||||||
:matched INTEGER
|
|
||||||
:updated INTEGER
|
|
||||||
:skipped ((:node-id ID :reason no-region) ...)
|
|
||||||
:reports (REPORT ...))
|
|
||||||
```
|
|
||||||
|
|
||||||
An explicit multi-update batch can use the lower-level incremental batch
|
|
||||||
functions only when an application deliberately owns that boundary; the
|
|
||||||
normal public alternative is `ebox-selector-update-buffer`.
|
|
||||||
|
|
||||||
### Logical candidates
|
|
||||||
|
|
||||||
Candidates are one-shot transactions based on the exact currently published
|
|
||||||
runtime. They are useful when several stable subtrees must be replaced before
|
|
||||||
one commit:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(let* ((buffer (get-buffer "*Ebox Demo*"))
|
|
||||||
(candidate (ebox-candidate-begin buffer))
|
|
||||||
(match (car (ebox-selector-query-buffer buffer "#body"))))
|
|
||||||
(ebox-candidate-replace
|
|
||||||
candidate
|
|
||||||
(plist-get match :node-id)
|
|
||||||
(ebox-create :id "body" :content "Candidate replacement"))
|
|
||||||
(ebox-commit buffer candidate))
|
|
||||||
```
|
|
||||||
|
|
||||||
Frameworks that retain semantic owners separately from backend anchors can
|
|
||||||
use `ebox-range-ref-present-p` as a read-only check before choosing a wider
|
|
||||||
published owner. It returns nil when the semantic Range is nested inside a
|
|
||||||
material anchor and therefore is not independently addressable in the current
|
|
||||||
Ebox publication.
|
|
||||||
|
|
||||||
`ebox-candidate-replace-host-ref` uses an application-owned `:host-ref`
|
|
||||||
instead of a runtime node id. Both replacement functions accept optional
|
|
||||||
`old-semantic-key` and `new-semantic-key` pairs for bounded detached identity
|
|
||||||
reuse. A candidate is sealed by commit and cannot be reused; it also becomes
|
|
||||||
stale if the captured buffer runtime or buffer tick changes.
|
|
||||||
|
|
||||||
`ebox-candidate-patch-host-paint` compares previous and next declarative Host
|
|
||||||
outputs and records a node-local patch only when every changed field is
|
|
||||||
paint-only. It retains child/runtime identity, updates inherited foreground
|
|
||||||
state in one affected-subtree pass, and returns nil when ordinary subtree
|
|
||||||
replacement is required.
|
|
||||||
|
|
||||||
`ebox-candidate-replace-root` targets a private candidate-bound root address.
|
|
||||||
It accepts exactly one declarative node, clears caller runtime identities, and
|
|
||||||
is last-wins. The final root replacement absorbs descendant node and host-ref
|
|
||||||
operations recorded before or after it, with no wrapper or public reference.
|
|
||||||
|
|
||||||
`ebox-child-range` creates a non-node segment descriptor in a material child
|
|
||||||
list. Its non-nil ref is root-global and its items participate directly in the
|
|
||||||
parent's key, selector, style, and layout scope. Use
|
|
||||||
`ebox-candidate-replace-range-ref` with a proper declarative node list to
|
|
||||||
replace that base payload. Empty payloads remain addressable.
|
|
||||||
|
|
||||||
### Host-reference positions
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-host-ref-position "*Ebox Demo*" 'toolbar)
|
|
||||||
;; => first live character position, or nil
|
|
||||||
(ebox-host-ref-bounds "*Ebox Demo*" 'toolbar)
|
|
||||||
;; => (START . END), excluding margins, or nil
|
|
||||||
```
|
|
||||||
|
|
||||||
Positions and bounds belong to the current publication generation. Obtain new
|
|
||||||
values after any update. Ebox's host-reference table is distinct from logical
|
|
||||||
selector ids.
|
|
||||||
|
|
||||||
### Reports and viewport reflow
|
|
||||||
|
|
||||||
Frameworks that allocate a new view tree before calling Ebox can include the
|
|
||||||
whole input callback in Ebox's render GC budget:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-call-with-render-burst
|
|
||||||
(lambda ()
|
|
||||||
(let ((next-root (build-framework-view)))
|
|
||||||
(ebox-commit buffer next-root))))
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-call-with-render-burst` is the preferred exception-safe boundary. When
|
|
||||||
the operation cannot be expressed as one function call,
|
|
||||||
`ebox-render-burst-begin` returns an opaque token for
|
|
||||||
`ebox-render-burst-end`; callers must use `unwind-protect` and end nested
|
|
||||||
tokens in last-in, first-out order. Interactive bursts share Ebox commit's
|
|
||||||
deferred state and restore the caller's GC settings 0.2 seconds after the
|
|
||||||
outermost end. A new burst cancels and supersedes the pending restore. Batch
|
|
||||||
bursts restore the exact threshold and percentage immediately and do not run
|
|
||||||
GC at the boundary. These functions only define allocation/GC ownership;
|
|
||||||
they do not publish, commit, or expose Ebox runtime state.
|
|
||||||
|
|
||||||
`ebox-buffer-update-report` returns a defensive copy of the last successful
|
|
||||||
report for a mounted buffer; it signals for a missing/non-Ebox buffer. A fresh
|
|
||||||
mount has no update report until its first update. Reports include the Ebox
|
|
||||||
strategy and publication scope plus TP surface revision and physical
|
|
||||||
operation/reconciliation facts.
|
|
||||||
|
|
||||||
The complete commit signature is `(ebox-commit BUFFER NEXT-ROOT &optional
|
|
||||||
FRAMEWORK-PUBLISH FRAMEWORK-ROLLBACK)`. Publish receives the report after the
|
|
||||||
buffer, TP surface, and Ebox runtime agree. If a later phase fails, rollback
|
|
||||||
receives that same report at most once; rollback errors and quits are contained.
|
|
||||||
`:framework-participant-state`, `:framework-participant-diagnostics`, and
|
|
||||||
`:scroll-finalization-diagnostics` are read-only outcome fields.
|
|
||||||
|
|
||||||
Use `ebox-rerender-buffer-with-context` when a root depends on a new viewport:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-rerender-buffer-with-context
|
|
||||||
(get-buffer "*Ebox Demo*")
|
|
||||||
800 ; viewport width in pixels
|
|
||||||
30) ; optional viewport height in lines
|
|
||||||
```
|
|
||||||
|
|
||||||
This preserves node/region identity and routes the change through viewport
|
|
||||||
dirty planning. `(viewport)` and `(viewport-height)` are resolved from this
|
|
||||||
context. For pure rendering, callers may instead bind `ebox-viewport-width`
|
|
||||||
in pixels and `ebox-viewport-height` in lines around the render. Do not mutate
|
|
||||||
a published node tree in place.
|
|
||||||
|
|
||||||
Visible mounted buffers follow their displaying window automatically. Ebox
|
|
||||||
publishes every size event immediately through its retained incremental path.
|
|
||||||
The Emacs hook is serialized and reentrant calls are rejected; there is no
|
|
||||||
second host hook, timer, or viewport queue.
|
|
||||||
`ebox-viewport-window-width` returns the same display-safe pixel width used by
|
|
||||||
that controller, so hosts must not install a second resize hook or width rule.
|
|
||||||
The former `ebox-viewport-resize-delay` setting has been removed; immediate
|
|
||||||
delivery has no timer setting to migrate.
|
|
||||||
|
|
||||||
## 7. Selectors
|
|
||||||
|
|
||||||
`ebox-selector-parse` delegates to ECSS and returns its structured AST.
|
|
||||||
`ebox-selector-match-node-p` matches one declarative node against a selector
|
|
||||||
or AST. `ebox-selector-query-all` returns document-ordered matches from an
|
|
||||||
unmounted tree. `ebox-selector-query-buffer` queries the retained runtime of a
|
|
||||||
live buffer and adds `:buffer` and, when the node is editable, `:region-handle`
|
|
||||||
to each match record.
|
|
||||||
|
|
||||||
Supported selector syntax comes from ECSS:
|
|
||||||
|
|
||||||
- selector lists and compound type/id/class selectors;
|
|
||||||
- attribute presence and `=`, `~=`, `|=`, `^=`, `$=`, `*=` operators with `i`
|
|
||||||
and `s` flags;
|
|
||||||
- state pseudos such as `:hover` and `:active` through `:selector-state`;
|
|
||||||
- `:is(...)`, `:where(...)`, `:not(...)`, and relational `:has(...)`;
|
|
||||||
- descendant whitespace, child `>`, adjacent sibling `+`, and general sibling
|
|
||||||
`~` combinators.
|
|
||||||
|
|
||||||
Ebox node types are exposed as selector types `box`, `row`, `column`, `flex`,
|
|
||||||
`grid`, and `item`. `:id` and `:key` become built-in attributes. Additional
|
|
||||||
attributes must be supplied explicitly, for example
|
|
||||||
`:selector-attributes '((role . button))`; visible content, layout values,
|
|
||||||
and internal runtime slots are never inferred as selector attributes.
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-selector-query-buffer "*Ebox Demo*" ".toolbar > box.action")
|
|
||||||
(ebox-selector-query-buffer "*Ebox Demo*" "div:has(> .warning)")
|
|
||||||
(ebox-selector-update-buffer "*Ebox Demo*" "[role=button]"
|
|
||||||
:color "#2563EB")
|
|
||||||
```
|
|
||||||
|
|
||||||
The compatibility aliases `ebox-select-all` and `ebox-update-selector` refer
|
|
||||||
to `ebox-selector-query-buffer` and `ebox-selector-update-buffer`.
|
|
||||||
|
|
||||||
## 8. Scrolling and viewport state
|
|
||||||
|
|
||||||
Set `:overflow 'scroll` (the default) and a finite `:height` to create a
|
|
||||||
scroll window:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-create
|
|
||||||
:id "log"
|
|
||||||
:width '(420)
|
|
||||||
:height 8
|
|
||||||
:overflow 'scroll
|
|
||||||
:content (mapconcat #'identity lines "\n"))
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-scroll-down`, `ebox-scroll-up`, `ebox-scroll-page-down`, and
|
|
||||||
`ebox-scroll-page-up` operate on the innermost scroll region at point. They
|
|
||||||
delegate to ordinary Emacs scrolling when no Ebox region can consume the
|
|
||||||
command. `ebox-wheel-scroll-down` and `ebox-wheel-scroll-up` accept mouse
|
|
||||||
events, animate when `ebox-wheel-smooth-scroll` is non-nil, and otherwise
|
|
||||||
delegate to `mwheel-scroll`. `ebox-buffer-mode` installs `ebox-scroll-map`
|
|
||||||
locally; `ebox-render-to-buffer` enables the mode on its returned buffer.
|
|
||||||
|
|
||||||
`ebox-scroll-state` accepts a numeric region id and returns a read-only state
|
|
||||||
plist for source-model mapping. Its useful public facts include the current
|
|
||||||
`:scroll-offset`, content height/lines, and cached visible lines. Treat the
|
|
||||||
plist as diagnostic/read-only data; use `ebox-region-update` with
|
|
||||||
`:scroll-offset` or the scroll commands to change position.
|
|
||||||
|
|
||||||
Scroll content is lazily prefetched and can be rendered in bounded slices.
|
|
||||||
Ebox preserves cached scroll state across safe retained updates and rolls it
|
|
||||||
back with the rest of a failed publication.
|
|
||||||
|
|
||||||
## 9. Measurement and cache controls
|
|
||||||
|
|
||||||
`ebox-string-pixel-width` returns the first-line display pixel width of a
|
|
||||||
string, honoring text scale and fixed `display` spaces. It is the same public
|
|
||||||
measurement primitive used by layout. `ebox-display-signature` returns the
|
|
||||||
current display inputs used for measurement cache invalidation. Call
|
|
||||||
`ebox-clear-cache` after changing font or named-face metrics outside the normal
|
|
||||||
display-signature signals.
|
|
||||||
|
|
||||||
The main customization variables are:
|
|
||||||
|
|
||||||
| Group | Variables and defaults |
|
|
||||||
| --- | --- |
|
|
||||||
| Render cache | `ebox-render-cache-max-entries` 2048; `ebox-render-cache-max-bytes` 32 MiB; `ebox-render-root-cache-max-entries` 16; `ebox-render-root-cache-max-bytes` 8 MiB |
|
|
||||||
| Keyboard/mouse scroll | `ebox-scroll-step` 1; `ebox-wheel-scroll-step` 1; `ebox-wheel-smooth-scroll` nil; `ebox-wheel-smooth-scroll-interval` 0.016; `ebox-wheel-smooth-scroll-lines-per-tick` 4; `ebox-wheel-smooth-scroll-target-ticks` 8. Keyboard uses point; wheel uses event position; residual lines bubble through nested owners. An interactive complete, chrome-free root document owner may be idle-materialized once and then use Emacs's native window line start; ordinary updates/resizes only warm retained indexes and never start a second native publication; nested/lazy/chrome owners retain transactional Ebox publication. |
|
|
||||||
| Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8; `ebox-scroll-lazy-idle-prefetch-lines` 128; `ebox-scroll-lazy-idle-prefetch-slice-lines` 16; `ebox-scroll-lazy-idle-prefetch-delay` 0.15 |
|
|
||||||
| Runtime prewarm | `ebox-runtime-idle-prewarm` t; `ebox-runtime-idle-prewarm-delay` 0.1; `ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0; `ebox-runtime-idle-prewarm-slice-size` 32; `ebox-native-buffer-scroll` t (initial/visible-window handoff only; strict root-owner proof) |
|
|
||||||
| Predicted reflow | `ebox-runtime-idle-reflow-cache-prewarm` t; `ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 |
|
|
||||||
| Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto`; `ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2; `ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB; `ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB; `ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB; `ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24; `ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 |
|
|
||||||
| Visual verification | `ebox-visual-check-output-dir` points to the temporary directory used for optional screenshots and reports. |
|
|
||||||
|
|
||||||
Idle prewarming never publishes buffer text or runtime state. Set the relevant
|
|
||||||
boolean to nil when a host application must avoid background preparation.
|
|
||||||
|
|
||||||
## 10. Standalone `.ebox` DSL
|
|
||||||
|
|
||||||
`ebox-build` compiles a data-oriented list form. Supported tags are `box` and
|
|
||||||
`ebox`, `row`, `column`, `flex`, `item`, `grid`, and `spacer`:
|
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-build
|
(ebox-build
|
||||||
'(grid :width (640)
|
'(column :padding (1 2)
|
||||||
:grid-template-columns ((200) 1fr 1fr)
|
(box :id "status" :color "#166534" "Ready")
|
||||||
:gap (1 (12))
|
(row :item-gap 1
|
||||||
(box :grid-column (1 :span 3) :content "Header")
|
(box "Left")
|
||||||
(box :content "Main")))
|
(box "Right"))))
|
||||||
```
|
```
|
||||||
|
|
||||||
Strings directly under `box` become newline-joined content. Child forms become
|
The author source fields are `:key`, `:class`, and `:id`. `:outer` is `inline`
|
||||||
a default vertical child layout. `row` and `column` can receive box
|
or `block` and controls how a Box participates in its parent.
|
||||||
properties around their child layout. `flex` accepts container properties;
|
|
||||||
`item` accepts flex item properties and exactly one or more child forms;
|
|
||||||
`spacer` accepts no children.
|
|
||||||
|
|
||||||
Inside a `.ebox` file, keep structural forms unquoted. Quote list and symbol
|
### Layout-owned properties
|
||||||
constants in property positions when they are data, such as
|
|
||||||
`:gap '(1 (12))` or `:justify-content 'center`; executable Elisp expressions
|
|
||||||
remain unquoted for the sibling playground runner.
|
|
||||||
|
|
||||||
## 11. Optional Rust reflow
|
| Form or relationship | Properties |
|
||||||
|
| --- | --- |
|
||||||
|
| `row`, `column` | `:item-gap`, `:cross-align` |
|
||||||
|
| `flex` | `:flex-direction`, `:flex-wrap`, `:flex-flow`, `:justify-content`, `:align-items`, `:align-content`, `:gap`, `:row-gap`, `:column-gap` |
|
||||||
|
| Direct child Box of `flex` | `:order`, `:flex`, `:flex-grow`, `:flex-shrink`, `:flex-basis`, `:align-self` |
|
||||||
|
| `grid` | `:grid-template-columns`, `:grid-template-rows`, `:grid-auto-columns`, `:grid-auto-rows`, `:grid-auto-flow`, `:justify-items`, `:justify-content`, `:align-items`, `:align-content`, `:gap`, `:row-gap`, `:column-gap` |
|
||||||
|
| Direct child Box of `grid` | `:order`, `:grid-column`, `:grid-row`, `:grid-column-span`, `:grid-row-span`, `:align-self` |
|
||||||
|
|
||||||
The native module is an accelerator, not a correctness dependency. Its Rust
|
Participation properties are ordinary child Box properties. They do not create
|
||||||
boundary receives bounded numeric layout work only; Ebox keeps the Elisp path
|
another public node or wrapper.
|
||||||
as the exact fallback and never builds Rust while loading.
|
|
||||||
|
### Common current properties
|
||||||
|
|
||||||
|
| Area | Current inputs |
|
||||||
|
| --- | --- |
|
||||||
|
| Size | `:box-sizing`, `:width`, `:min-width`, `:max-width`, `:height`, `:min-height`, `:max-height` |
|
||||||
|
| Edges | `:padding`, `:margin`, `:border`, side shorthands, and their longhands |
|
||||||
|
| Paint | `:color`, `:background-color`, exact alias `:bgcolor`, border colors, `:visibility` |
|
||||||
|
| Text layout | `:text-align`, `:wrap-mode` (`word`, `char`, `kp`, `none`) |
|
||||||
|
| Overflow | `:overflow` (`visible`, `hidden`, `scroll`) |
|
||||||
|
|
||||||
|
Ordinary horizontal numbers are character columns; a one-element horizontal
|
||||||
|
list is pixels. Vertical numbers are lines. `(viewport)` and
|
||||||
|
`(viewport-height)` use the current render context. Property validation belongs
|
||||||
|
to Ebox and invalid context/value combinations signal an error.
|
||||||
|
|
||||||
|
## 2. Evaluated typed construction
|
||||||
|
|
||||||
|
Frameworks that already own author parsing can construct canonical nodes
|
||||||
|
directly:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-native-status) ; show toolchain, path, ABI, and load diagnosis
|
(let ((layout (ebox-column-layout-create
|
||||||
(ebox-native-build) ; asynchronous build/install
|
:item-gap 1 :cross-align 'stretch)))
|
||||||
(ebox-native-build t) ; clean Ebox's private Cargo cache first
|
(ebox-box-create
|
||||||
|
:layout layout
|
||||||
|
:children (list (ebox-text-create :value "One")
|
||||||
|
(ebox-text-create :value "Two"))))
|
||||||
```
|
```
|
||||||
|
|
||||||
The equivalent repository command is `make native-build`. Configure
|
- `ebox-text-create` requires `:value STRING`.
|
||||||
`ebox-native-reflow-module-path` with a module file or directory. Session
|
- `ebox-normal-layout-create` creates Normal LayoutConfig.
|
||||||
limits are `ebox-native-reflow-max-jobs` 512,
|
- `ebox-row-layout-create` and `ebox-column-layout-create` accept
|
||||||
`ebox-native-reflow-max-results` 512, and
|
`:item-gap` and `:cross-align`.
|
||||||
`ebox-native-reflow-max-result-bytes` 64 MiB. A missing or incompatible module
|
- `ebox-flex-layout-create` creates a validated Flex LayoutConfig.
|
||||||
leaves Ebox on Elisp; `ebox-native-status` reports the reason.
|
- `ebox-grid-layout-create` creates a validated Grid LayoutConfig.
|
||||||
|
- `ebox-box-create` requires one typed `:layout` value and accepts a
|
||||||
|
`:children` list of typed nodes.
|
||||||
|
|
||||||
`ebox-byte-compile` recompiles all active Ebox Elisp sources and does not build
|
The last two fields belong only to this evaluated integration contract. They
|
||||||
Rust. Restart Emacs after a successful byte compilation if the current
|
are not author properties or alternate DSL syntax.
|
||||||
process should load the new `.elc` files.
|
|
||||||
|
|
||||||
## 12. Public function inventory
|
Node inspection functions are `ebox-node-kind`, `ebox-text-node-p`,
|
||||||
|
`ebox-box-node-p`, `ebox-node-source-handle`, `ebox-text-node-value`,
|
||||||
|
`ebox-box-node-layout`, `ebox-box-node-children`, and
|
||||||
|
`ebox-box-node-range-anchors`. They are public from the typed module even
|
||||||
|
though the facade inventory below focuses on application entry points.
|
||||||
|
|
||||||
The following table covers the public functions in the facade inventory. The
|
## 3. Rendering and retained publication
|
||||||
style-rule functions immediately below are public module-level style APIs.
|
|
||||||
|
|
||||||
| Function | Use |
|
| Function | Contract |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `ebox-text-create`, `ebox-normal-layout-create`, `ebox-row-layout-create`, `ebox-column-layout-create`, `ebox-flex-layout-create`, `ebox-grid-layout-create`, `ebox-box-create` | Framework-integration port for typed TextNode, Normal/Row/Column/Flex/Grid LayoutConfig, and BoxNode values. |
|
| `ebox-render` | Return a propertized string without publishing a live buffer. |
|
||||||
| `ebox-create`, `ebox-build` | Create a legacy node or compile the `.ebox` list DSL during migration. |
|
| `ebox-render-to-buffer` | Copy the built root, mount a retained TP surface, enable `ebox-buffer-mode`, and return the buffer. |
|
||||||
| `ebox-concat`, `ebox-stack`, `ebox-row`, `ebox-column`, `ebox-spacer` | Compose simple horizontal/vertical layouts and blank boxes. |
|
| `ebox-display-buffer` | Render through the retained path, delete other windows, and switch to the result. |
|
||||||
| `ebox-flex`, `ebox-flex-item` | Build flex containers and item metadata. |
|
| `ebox-commit` | Atomically publish a newly built root or one-shot logical candidate. |
|
||||||
| `ebox-grid`, `ebox-grid-fr` | Build Grid layouts and fractional tracks; placement metadata belongs to direct child Boxes. |
|
| `ebox-buffer-update-report` | Return a defensive copy of the last successful update report. |
|
||||||
| `ebox-render`, `ebox-render-to-buffer`, `ebox-display-buffer` | Pure materialization, retained mount, and display wrapper. |
|
| `ebox-rerender-buffer-with-context` | Apply explicit viewport width and optional height while retaining identity. |
|
||||||
| `ebox-commit`, `ebox-buffer-update-report`, `ebox-rerender-buffer-with-context`, `ebox-viewport-window-width` | Atomic root commit, report lookup, viewport-context rerender, and the shared display-safe width sampler. |
|
| `ebox-viewport-window-width` | Return Ebox's display-safe pixel width for a window. |
|
||||||
| `ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update` | Region mapping, logical handle lookup, and direct update. |
|
|
||||||
| `ebox-child-range`, `ebox-range-ref-present-p`, `ebox-candidate-begin`, `ebox-candidate-replace`, `ebox-candidate-replace-range-ref`, `ebox-candidate-replace-root`, `ebox-candidate-replace-host-ref`, `ebox-candidate-patch-host-paint` | Persistent material child segments, backend-anchor lookup, subtree replacement, and retained Host paint transactions. |
|
|
||||||
| `ebox-host-ref-bounds`, `ebox-host-ref-position` | Live host-reference bounds and first position. |
|
|
||||||
| `ebox-selector-parse`, `ebox-selector-match-node-p`, `ebox-selector-query-all`, `ebox-selector-query-buffer`, `ebox-selector-update-buffer` | ECSS selector compilation, matching, querying, and batch update. |
|
|
||||||
| `ebox-select-all`, `ebox-update-selector` | Compatibility aliases for the two buffer selector functions. |
|
|
||||||
| `ebox-buffer-mode`, `ebox-scroll-map`, `ebox-scroll-down`, `ebox-scroll-up`, `ebox-scroll-page-down`, `ebox-scroll-page-up`, `ebox-wheel-scroll-down`, `ebox-wheel-scroll-up`, `ebox-scroll-state` | Interactive scrolling, local keymap, and read-only scroll state. |
|
|
||||||
| `ebox-string-pixel-width`, `ebox-display-signature`, `ebox-clear-cache` | Display-aware measurement and cache management. |
|
|
||||||
| `ebox-native-build`, `ebox-native-status`, `ebox-byte-compile` | Optional native workflow and Elisp byte compilation. |
|
|
||||||
|
|
||||||
Style-rule and style inspection entry points available after `(require 'ebox)`
|
`ebox-commit` accepts optional framework publish and rollback callbacks. They
|
||||||
include `ebox-style-add-rule`, `ebox-style-reset-rules`,
|
join the existing atomic publication; failures restore the previous buffer,
|
||||||
`ebox-style-cascade-active-p`, `ebox-style-compute`,
|
surface, runtime state, and successful report.
|
||||||
`ebox-style-expand-shorthands`, `ebox-style-compile-declarations`,
|
|
||||||
`ebox-style-merge-declarations`, `ebox-style-compute-subject`,
|
|
||||||
`ebox-style-property`, `ebox-style-canonical-name`, and
|
|
||||||
`ebox-style-dirty-kind`. The first two are the normal application entry
|
|
||||||
points; the remaining helpers are for integrations that need to inspect or
|
|
||||||
prepare ECSS-backed style data.
|
|
||||||
|
|
||||||
## 13. Verification and support boundary
|
`ebox-call-with-render-burst` is the exception-safe allocation/GC boundary for
|
||||||
|
a framework callback. `ebox-render-burst-begin` and `ebox-render-burst-end`
|
||||||
|
provide the lower-level token form and must be paired with `unwind-protect`.
|
||||||
|
They do not publish by themselves.
|
||||||
|
|
||||||
From the repository root:
|
## 4. Identity, direct updates, and logical candidates
|
||||||
|
|
||||||
|
| Function | Contract |
|
||||||
|
| --- | --- |
|
||||||
|
| `ebox-region-ids` | Return current low-level region ids in document order. |
|
||||||
|
| `ebox-region-resolve` | Resolve one logical `:id` in a mounted buffer to an opaque surface-scoped handle. |
|
||||||
|
| `ebox-region-update` | Apply supported mutable style or scroll properties through one retained transaction. |
|
||||||
|
| `ebox-child-range` | Build a transparent, addressable child Range descriptor from typed nodes. |
|
||||||
|
| `ebox-candidate-begin` | Capture the exact current mounted generation in a one-shot candidate. |
|
||||||
|
| `ebox-candidate-replace` | Replace one candidate node address. |
|
||||||
|
| `ebox-candidate-replace-range-ref` | Replace one transparent child Range payload. |
|
||||||
|
| `ebox-candidate-replace-root` | Replace the candidate's private root address. |
|
||||||
|
| `ebox-candidate-replace-host-ref` | Replace one framework-owned opaque host reference. |
|
||||||
|
| `ebox-candidate-patch-host-paint` | Record a proven paint-only host patch or return nil when replacement is required. |
|
||||||
|
| `ebox-host-ref-bounds` | Return current margin-free bounds for an opaque host reference. |
|
||||||
|
| `ebox-host-ref-position` | Return its first current position. |
|
||||||
|
|
||||||
|
Handles and reported positions belong to one live publication generation.
|
||||||
|
Resolve them again after their object is removed. A candidate is sealed by
|
||||||
|
commit and cannot be reused.
|
||||||
|
|
||||||
|
## 5. Selectors
|
||||||
|
|
||||||
|
ECSS is the sole selector parser and matcher. Ebox supplies logical tree
|
||||||
|
relations and mounted indexes.
|
||||||
|
|
||||||
|
| Function | Contract |
|
||||||
|
| --- | --- |
|
||||||
|
| `ebox-selector-parse` | Compile a CSS-like selector to the ECSS AST. |
|
||||||
|
| `ebox-selector-match-node-p` | Match one built node against a selector or AST. |
|
||||||
|
| `ebox-selector-query-all` | Query an unmounted built tree in document order. |
|
||||||
|
| `ebox-selector-query-buffer` | Query one mounted runtime. |
|
||||||
|
| `ebox-selector-update-buffer` | Update all editable mounted matches and return a structured report. |
|
||||||
|
| `ebox-select-all` | Alias of `ebox-selector-query-buffer`. |
|
||||||
|
| `ebox-update-selector` | Alias of `ebox-selector-update-buffer`. |
|
||||||
|
|
||||||
|
## 6. Scrolling
|
||||||
|
|
||||||
|
`ebox-buffer-mode` installs `ebox-scroll-map`. The map routes line, page, and
|
||||||
|
wheel intent through the innermost semantic scroll owner before ordinary Emacs
|
||||||
|
scrolling.
|
||||||
|
|
||||||
|
The public commands are `ebox-scroll-down`, `ebox-scroll-up`,
|
||||||
|
`ebox-scroll-page-down`, `ebox-scroll-page-up`, `ebox-wheel-scroll-down`, and
|
||||||
|
`ebox-wheel-scroll-up`. `ebox-scroll-state` returns a read-only snapshot for a
|
||||||
|
numeric region id.
|
||||||
|
|
||||||
|
## 7. Measurement, cache, and build tools
|
||||||
|
|
||||||
|
| Function | Contract |
|
||||||
|
| --- | --- |
|
||||||
|
| `ebox-string-pixel-width` | Measure the first-line display width of a string. |
|
||||||
|
| `ebox-display-signature` | Return display inputs used for measurement-cache invalidation. |
|
||||||
|
| `ebox-clear-cache` | Clear Ebox measurement/render caches. |
|
||||||
|
| `ebox-byte-compile` | Compile active Ebox Elisp sources; restart Emacs to load the new bytecode. |
|
||||||
|
| `ebox-native-status` | Report native toolchain, module, ABI, and load status. |
|
||||||
|
| `ebox-native-build` | Build and install the optional native module asynchronously. |
|
||||||
|
|
||||||
|
The native module is an accelerator, never a correctness requirement. Ebox
|
||||||
|
does not build it during package load.
|
||||||
|
|
||||||
|
## 8. Facade inventory
|
||||||
|
|
||||||
|
`ebox-public-api` contains the stable facade entries. The following grouping
|
||||||
|
names every entry:
|
||||||
|
|
||||||
|
- Construction: `ebox-build`, `ebox-text-create`,
|
||||||
|
`ebox-normal-layout-create`, `ebox-row-layout-create`,
|
||||||
|
`ebox-column-layout-create`, `ebox-flex-layout-create`,
|
||||||
|
`ebox-grid-layout-create`, `ebox-box-create`, `ebox-child-range`.
|
||||||
|
- Render/publication: `ebox-render`, `ebox-render-to-buffer`,
|
||||||
|
`ebox-display-buffer`, `ebox-commit`, `ebox-buffer-update-report`,
|
||||||
|
`ebox-rerender-buffer-with-context`, `ebox-viewport-window-width`,
|
||||||
|
`ebox-call-with-render-burst`, `ebox-render-burst-begin`,
|
||||||
|
`ebox-render-burst-end`.
|
||||||
|
- Candidate/identity: `ebox-candidate-begin`, `ebox-candidate-replace`,
|
||||||
|
`ebox-candidate-replace-range-ref`, `ebox-candidate-replace-root`,
|
||||||
|
`ebox-candidate-replace-host-ref`, `ebox-candidate-patch-host-paint`,
|
||||||
|
`ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update`,
|
||||||
|
`ebox-host-ref-bounds`, `ebox-host-ref-position`.
|
||||||
|
- Selectors: `ebox-selector-parse`, `ebox-selector-match-node-p`,
|
||||||
|
`ebox-selector-query-all`, `ebox-selector-query-buffer`,
|
||||||
|
`ebox-selector-update-buffer`, `ebox-select-all`, `ebox-update-selector`.
|
||||||
|
- Scroll: `ebox-buffer-mode`, `ebox-scroll-map`, `ebox-scroll-down`,
|
||||||
|
`ebox-scroll-up`, `ebox-scroll-page-down`, `ebox-scroll-page-up`,
|
||||||
|
`ebox-wheel-scroll-down`, `ebox-wheel-scroll-up`, `ebox-scroll-state`.
|
||||||
|
- Measurement/build: `ebox-string-pixel-width`, `ebox-display-signature`,
|
||||||
|
`ebox-clear-cache`, `ebox-byte-compile`, `ebox-native-status`,
|
||||||
|
`ebox-native-build`.
|
||||||
|
|
||||||
|
## 9. Verification
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
|
||||||
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make native-rust-tests
|
make dsl-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
|
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
```
|
```
|
||||||
|
|
||||||
Focused targets include `make core-tests`, `make grid-tests`,
|
|
||||||
`make surface-tests`, `make selector-tests`, `make dsl-tests`,
|
|
||||||
`make flex-tests`, and `make visual-check-tests`. The sibling
|
|
||||||
`ebox-playground` package owns runnable example fixtures; ETAF owns
|
|
||||||
Components, state, controls, and application behavior. Do not call private
|
|
||||||
`ebox--*` functions or edit the historical Ebox checkout as part of
|
|
||||||
this package.
|
|
||||||
|
|||||||
@ -2,572 +2,210 @@
|
|||||||
|
|
||||||
[English](ebox-api-reference.en.md)
|
[English](ebox-api-reference.en.md)
|
||||||
|
|
||||||
本文是独立 Ebox 包面向用户的完整清单,覆盖节点模型、property、布局
|
本文描述独立 Ebox 包当前的公共边界,并把普通 author 路径与框架集成使用的
|
||||||
辅助函数、渲染/发布、selector、滚动、测量、DSL 和可选 native 模块。
|
evaluated typed API 分开。以 `ebox--` 开头的名称都是私有实现。
|
||||||
`ebox--*` 名称与没有在本文说明的模块辅助函数属于实现细节。加载
|
|
||||||
`(require 'ebox)` 后,也可以通过 `ebox-public-api` 查看门面入口清单。
|
|
||||||
|
|
||||||
Ebox 负责 box node、样式归一化、测量、布局、渲染、语义更新规划、selector
|
Ebox 需要 Emacs 29.1 或更高版本、ECSS 和 TP:
|
||||||
适配、Grid 和可选 Rust reflow 加速器。TP 负责 live buffer 发布、物理 diff、
|
|
||||||
revision 和 rollback;ECSS 负责 selector 解析、匹配与 cascade。Ebox 不提供
|
|
||||||
Component、应用状态、UI control 或应用生命周期。
|
|
||||||
|
|
||||||
## 1. 安装与加载
|
|
||||||
|
|
||||||
Ebox 需要 Emacs 29.1 或更新版本,以及 ECSS 和 TP。使用同级源码 checkout:
|
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(add-to-list 'load-path "/path/to/github/ecss")
|
|
||||||
(add-to-list 'load-path "/path/to/github/tp")
|
|
||||||
(add-to-list 'load-path "/path/to/github/ebox")
|
|
||||||
(require 'ebox)
|
(require 'ebox)
|
||||||
```
|
```
|
||||||
|
|
||||||
加载 Ebox 会加载 Elisp 布局/runtime 模块,但不会创建 buffer、在当前 buffer
|
## 1. Author 语法
|
||||||
启用 mode、构建 Rust 或修改当前编辑 buffer。可选 native 模块只在其工作流
|
|
||||||
或合资格的 native 路径需要时加载。
|
|
||||||
|
|
||||||
## 2. 常规生命周期
|
`ebox-build` 只接受以下 author 入口:
|
||||||
|
|
||||||
公共工作流如下:
|
```text
|
||||||
|
STRING
|
||||||
```elisp
|
(text PROPERTY VALUE ... STRING)
|
||||||
(let ((root
|
(box PROPERTY VALUE ... CHILD ...)
|
||||||
(ebox-column
|
(row PROPERTY VALUE ... CHILD ...)
|
||||||
(ebox-create :id "status" :content "Ready" :width '(240))
|
(column PROPERTY VALUE ... CHILD ...)
|
||||||
(ebox-create :content "调用者拥有的 source tree"))))
|
(flex PROPERTY VALUE ... CHILD ...)
|
||||||
;; 纯 materialization;返回多行带属性字符串。
|
(grid PROPERTY VALUE ... CHILD ...)
|
||||||
(ebox-render root)
|
|
||||||
|
|
||||||
;; retained TP surface;返回 live buffer。
|
|
||||||
(ebox-render-to-buffer "*Ebox Demo*" root)
|
|
||||||
|
|
||||||
;; 后续构造新的声明式根树并原子发布。
|
|
||||||
(ebox-commit
|
|
||||||
"*Ebox Demo*"
|
|
||||||
(ebox-column
|
|
||||||
(ebox-create :id "status" :content "Updated" :width '(240))
|
|
||||||
(ebox-create :content "未修改调用者的 source tree。"))))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`ebox-render` 不产生 live buffer 副作用。没有 active stylesheet 和 inline
|
`STRING` 是 `(text STRING)` 的简写。Text form 必须且只能有一个字符串 payload。
|
||||||
继承样式依赖的静态树可以使用隔离的 pure materialization;依赖 stylesheet
|
每种 Box form 都接受零个或多个直接嵌套的 Text/Box 子节点。form 名称选择 Normal、
|
||||||
的 pure render 使用临时 TP materialization。两者都遵守同一个公共字符串
|
Row、Column、Flex 或 Grid 子布局;author 不通过 property 传入布局选择器。
|
||||||
契约。两个 buffer 入口都使用 retained TP surface。Ebox 会在分配 runtime
|
|
||||||
identity 前复制声明式输入,因此同一 source tree 可以挂载到多个 buffer。
|
|
||||||
|
|
||||||
`ebox-display-buffer` 是同时需要展示 buffer 时的便捷包装;它会先删除其他
|
|
||||||
window,再切换到渲染后的 buffer。如果窗口选择由调用方负责,应使用
|
|
||||||
`ebox-render-to-buffer`。
|
|
||||||
|
|
||||||
## 3. 节点与组合
|
|
||||||
|
|
||||||
### Box 与简单布局构造器
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-create &rest properties)
|
|
||||||
(ebox-concat node-1 node-2) ; 两个节点横向排列
|
|
||||||
(ebox-stack node-1 node-2) ; 两个节点纵向排列
|
|
||||||
(ebox-row &rest nodes) ; 多个节点横向排列;单个 Range 仍保留 row parent
|
|
||||||
(ebox-column &rest nodes) ; 多个节点纵向排列;单个 Range 仍保留 column parent
|
|
||||||
(ebox-spacer &rest properties) ; 空白 box
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-create` 返回声明式 box node。`:content` 是字符串内容;容器子节点通常
|
|
||||||
交给 `ebox-row`、`ebox-column`、`ebox-flex` 或 `ebox-grid`。`ebox-concat` 和
|
|
||||||
`ebox-stack` 接受两个节点;超过两个子节点时使用 row/column。nil 子节点会
|
|
||||||
被忽略;空 row/column 会变成 spacer。
|
|
||||||
|
|
||||||
`row` 中省略或 `auto` 的宽度按子节点自身的 intrinsic width 参与横向拼接;
|
|
||||||
父级 viewport 不会被重复当成每个子节点的宽度。显式 `viewport`、`stretch`
|
|
||||||
或 definite width 仍按指定 containing block 解析。`column` 会把较窄的布局
|
|
||||||
行补齐到自己的 containing block,但带视觉 box 的子节点会在自己的 used width
|
|
||||||
上绘制边框和 padding。
|
|
||||||
|
|
||||||
`:key` 是兄弟节点范围内的稳定业务 identity;`:id` 是 logical selector/update
|
|
||||||
标识;`:host-ref` 是应用拥有的不透明锚点,用于 host-reference 查询和逻辑
|
|
||||||
candidate replacement。一个 source tree 内的非 nil host-ref 必须唯一。
|
|
||||||
|
|
||||||
### Flex
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-flex
|
|
||||||
:width '(480)
|
|
||||||
:flex-flow '(row wrap)
|
|
||||||
:gap '(1 (12))
|
|
||||||
(ebox-flex-item (ebox-create :content "A")
|
|
||||||
:flex '(1 1 auto)
|
|
||||||
:align-self 'center)
|
|
||||||
(ebox-create :content "B" :flex-grow 2))
|
|
||||||
```
|
|
||||||
|
|
||||||
容器 property 是 `:flex-direction`、`:flex-wrap`、`:flex-flow`、
|
|
||||||
`:justify-content`、`:align-items`、`:align-content`、`:gap`、`:row-gap` 和
|
|
||||||
`:column-gap`,另加 `:width`、`:height`、padding、border、paint 等普通 box
|
|
||||||
property。默认方向是 row、nowrap,主轴是 flex-start,交叉轴是 stretch。
|
|
||||||
|
|
||||||
item property 是 `:order`、`:flex`、`:flex-grow`、`:flex-shrink`、`:flex-basis`
|
|
||||||
和 `:align-self`。它们可以直接放在 child box 上,也可以通过
|
|
||||||
`ebox-flex-item` 附加。传给 `ebox-flex-item` 的非 item property 会在 child
|
|
||||||
外包一层普通 box。
|
|
||||||
|
|
||||||
Flex preferred/min/max size 支持 Ebox 子集:`auto`、`min-content`、
|
|
||||||
`max-content`、`fit-content`、`stretch`、`contain`;max-size 还支持
|
|
||||||
`none`。`fit-content` 可以带数字或 viewport 相对限制。这些是 Ebox 布局值,
|
|
||||||
不表示完整浏览器 CSS 兼容性。
|
|
||||||
|
|
||||||
### Grid
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-grid
|
|
||||||
:width '(640)
|
|
||||||
:grid-template-columns '((200) 1fr 1fr)
|
|
||||||
:grid-template-rows '(1 1)
|
|
||||||
:gap '(1 (12))
|
|
||||||
(ebox-create :content "Header" :grid-column '(1 :span 3))
|
|
||||||
(ebox-create :content "Main" :grid-column 1 :grid-row 2)
|
|
||||||
(ebox-create :content "Aside" :grid-column 2 :grid-row 2))
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-grid-fr` 构造分数轨道:`(ebox-grid-fr 2)` 返回 `(fr 2)`。Grid 轨道支持
|
|
||||||
固定尺寸、`auto`、`(fr FACTOR)`、`1fr` 这样的 symbol、`minmax` 和 `repeat`。
|
|
||||||
没有显式给出的行列使用隐式轨道;`:grid-auto-columns`、`:grid-auto-rows`
|
|
||||||
和 `:grid-auto-flow`(`row` 或 `column`)控制隐式放置。
|
|
||||||
|
|
||||||
Grid placement 从 1 开始。`:grid-column` 和 `:grid-row` 接受正整数起点、
|
|
||||||
`(start :span 正整数)`,或 `start` 小于 `end` 的 `(start end)`。
|
|
||||||
`:grid-column-span` 与 `:grid-row-span` 接受正整数。placement property 直接属于
|
|
||||||
child Box;Grid item 是父子参与角色,不是 wrapper 或 constructor。
|
|
||||||
|
|
||||||
## 4. 单位、box property 与文本属性
|
|
||||||
|
|
||||||
### 尺寸单位
|
|
||||||
|
|
||||||
| 值 | 含义 |
|
|
||||||
| --- | --- |
|
|
||||||
| 普通横向数字,例如 `12` | 字符列数,按当前 display 的空格宽度转换。 |
|
|
||||||
| 单元素横向 list,例如 `'(240)` | 绝对像素。 |
|
|
||||||
| 纵向数字,例如 `3` | 行数。 |
|
|
||||||
| `(viewport)` 或 `viewport` | 当前 viewport 的像素宽度。 |
|
|
||||||
| `(viewport-height)` 或 `viewport-height` | 当前 viewport 的行高。 |
|
|
||||||
| `auto`、`min-content`、`max-content`、`fit-content`、`stretch`、`contain` | 在对应 property 与 formatting context 支持时使用 intrinsic/preferred width。 |
|
|
||||||
|
|
||||||
`:box-sizing` 默认是 `border-box`,也支持 `content-box`。宽度 property 是
|
|
||||||
`:width`、`:min-width`、`:max-width`;高度 property 是 `:height`、
|
|
||||||
`:min-height`、`:max-height`。几何负值会被拒绝。依赖 viewport 的节点要在
|
|
||||||
viewport context 中渲染,或通过 `ebox-rerender-buffer-with-context` 重排。
|
|
||||||
|
|
||||||
### Box、paint 与文本 property
|
|
||||||
|
|
||||||
以下名称可用于 `ebox-create`。Stylesheet 规则使用其中的 style/geometry
|
|
||||||
property;`ebox-region-update` 接受其目标支持的可变 content/style/scroll
|
|
||||||
子集。Direct update 不会添加 child 或修改 selector metadata;结构变化使用
|
|
||||||
`ebox-commit` 或 logical candidate。结构 metadata、`:content` 和
|
|
||||||
`:surface-properties` 是节点/render 输入,不是 stylesheet declaration。表中
|
|
||||||
同时列出 canonical 名称与兼容别名。
|
|
||||||
|
|
||||||
| 范围 | Property |
|
|
||||||
| --- | --- |
|
|
||||||
| Content/geometry | `:content`、`:box-sizing`、`:width`、`:min-width`、`:max-width`、`:height`、`:min-height`、`:max-height` |
|
|
||||||
| Padding | `:padding`、`:padding-inline`、`:padding-block`;longhand `:padding-block-start`、`:padding-inline-end`、`:padding-block-end`、`:padding-inline-start`;别名 `:padding-top`、`:padding-right`、`:padding-bottom`、`:padding-left`,以及 `:padding-right-pixel`、`:padding-left-pixel`、`:padding-top-height`、`:padding-bottom-height` |
|
|
||||||
| Margin | `:margin`、`:margin-inline`、`:margin-block`;longhand `:margin-block-start`、`:margin-inline-end`、`:margin-block-end`、`:margin-inline-start`;别名 `:margin-top`、`:margin-right`、`:margin-bottom`、`:margin-left`,以及 `:margin-right-pixel`、`:margin-left-pixel`、`:margin-top-height`、`:margin-bottom-height` |
|
|
||||||
| Border | `:border`、`:border-top`、`:border-right`、`:border-bottom`、`:border-left`、`:border-width`、`:border-style`、`:border-color`;side longhand `:border-*-width`、`:border-*-style`、`:border-*-color`;Ebox boolean shorthand `:border-top-p`、`:border-bottom-p`;兼容别名 `:border-left-pixel`、`:border-right-pixel` |
|
|
||||||
| Paint | `:color`、`:background-color`、别名 `:bgcolor` |
|
|
||||||
| Typography | `:font`、`:font-family`、`:font-height` 和别名 `:font-size`、`:font-weight`、`:font-slant` |
|
|
||||||
| Box 内容 | `:text-align`(`left`、`center`、`right`)、`:vertical-align`(`top`、`center`/`middle`、`bottom`)、`:overflow`(`scroll`、`hidden`、`visible`)、`:wrap-mode`(`word`、`char`、`kp`、`none`,默认 `word`)、`:visibility`(`visible` 或 `hidden`) |
|
|
||||||
| Structural style | `:display` 会在 node/layout context 中计算;有效的 display tuple 通常由布局构造器选择。 |
|
|
||||||
| Flex container/item | `:flex-direction`、`:flex-wrap`、`:flex-flow`、`:justify-content`、`:align-items`、`:align-content`、`:gap`、`:row-gap`、`:column-gap`(别名 `:grid-row-gap`、`:grid-column-gap`);item 名称 `:order`、`:flex`、`:flex-grow`、`:flex-shrink`、`:flex-basis`、`:align-self` |
|
|
||||||
| Grid container/item | `:grid-template-columns`、`:grid-template-rows`、`:grid-auto-columns`、`:grid-auto-rows`、`:grid-auto-flow`、`:justify-items`;item placement `:grid-column`、`:grid-row`、`:grid-column-span`、`:grid-row-span` |
|
|
||||||
| 额外文本属性 | `:surface-properties` 接受偶数长度的 Emacs text-property plist,应用到不含换行的渲染字符;内层非 nil 属性优先。 |
|
|
||||||
|
|
||||||
Box 字体入口是 `:font`,不是 `:face`。`:font` 接受 Emacs face symbol、face
|
|
||||||
名称字符串或 face plist;typography longhand 会合并到该 face。`:visibility
|
|
||||||
'hidden` 会隐藏 ink,但保留布局 footprint 与 metadata。`:overflow 'visible`
|
|
||||||
会发布超出高度的可见行;`hidden` 会裁剪;`scroll` 暴露有限窗口,并在内容
|
|
||||||
高于 box 时启用 Ebox 滚动。
|
|
||||||
|
|
||||||
Padding 与 margin 使用 CSS 的 1–4 值展开。横向值使用上述横向单位,顶部和
|
|
||||||
底部使用行单位。左右 border 是像素几何;上下 border 通过 Emacs overline/
|
|
||||||
underline face 实现,因此宽度是存在性标志而非任意像素厚度。
|
|
||||||
|
|
||||||
### Node metadata 与 selector 输入
|
|
||||||
|
|
||||||
这些输入用于标识节点,不会直接改变几何:
|
|
||||||
|
|
||||||
| 输入 | 含义 |
|
|
||||||
| --- | --- |
|
|
||||||
| `:id` | logical selector/update id。symbol 和其他标量会归一化为字符串;直接 update 解析时应保持 id 唯一。 |
|
|
||||||
| `:key` | reconciliation 使用的稳定兄弟 identity,同时作为内建 selector `:key` attribute 暴露。 |
|
|
||||||
| `:class` | 一个 class token 或 class token 列表,用于 selector matching。 |
|
|
||||||
| `:selector-state` | 一个 state token 或 state token 列表,用于匹配 `:hover`、`:active` 等 state pseudo。 |
|
|
||||||
| `:selector-attributes` | 显式 attribute alist,例如 `((role . button))`;content、布局值和 runtime 字段不会自动推断。 |
|
|
||||||
| `:host-ref` | 应用拥有的唯一不透明 anchor,用于 `ebox-host-ref-position`、`ebox-host-ref-bounds` 和 host-ref candidate replacement。 |
|
|
||||||
| `:scroll-offset` | scroll region 的初始/受控行偏移。交互优先使用滚动命令;update 必须使用 live region handle。 |
|
|
||||||
|
|
||||||
`:id`、`:key` 和 `:host-ref` 不是 stylesheet declaration。`:class`、
|
|
||||||
`:selector-state` 和 `:selector-attributes` 是 selector metadata,不是通用
|
|
||||||
应用状态。
|
|
||||||
|
|
||||||
## 5. Stylesheet 与 cascade
|
|
||||||
|
|
||||||
多数节点只需 inline property。需要 selector 驱动样式时使用隔离的 Ebox
|
|
||||||
stylesheet:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-style-reset-rules)
|
|
||||||
(ebox-style-add-rule
|
|
||||||
".card"
|
|
||||||
'(:color "#1F2937"
|
|
||||||
:background-color "#F8FAFC"
|
|
||||||
:padding '(1 (12)))
|
|
||||||
:layer 'base)
|
|
||||||
(ebox-style-add-rule
|
|
||||||
".card:has(> .warning)"
|
|
||||||
'(:border-color "#DC2626")
|
|
||||||
:layer 'state)
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-style-add-rule` 接受 selector string 或 AST、Ebox declaration plist,
|
|
||||||
以及 ECSS cascade 关键字 `:origin`、`:layer`、`:scope`。
|
|
||||||
`ebox-style-reset-rules` 清空 stylesheet。规则属于已加载的全局 Ebox style
|
|
||||||
registry;改变规则不会自动发布已有 mounted buffer,因此规则变化后要对
|
|
||||||
目标 buffer rerender 或 commit。`ebox-style-cascade-active-p` 可检查当前
|
|
||||||
是否存在规则。
|
|
||||||
|
|
||||||
基于 ECSS 的 style domain 包含 color 与 typography inheritance、computed
|
|
||||||
value、cascade layer、custom-property declaration,以及 paint/geometry/
|
|
||||||
structure dirty 分类。Ebox 只解释本文列出的 property;百分比、绝对定位、
|
|
||||||
z-index、阴影、border radius、浏览器级 bidi 和完整浏览器 typography 不属于
|
|
||||||
本包。
|
|
||||||
|
|
||||||
名称以 `--` 开头的 custom property 可以由 ECSS style domain 携带,但除非宿主
|
|
||||||
集成把它解析为本文支持的 Ebox property,否则不会影响 Ebox 输出。
|
|
||||||
|
|
||||||
## 6. 渲染、identity 与更新
|
|
||||||
|
|
||||||
### Region id 与 logical handle
|
|
||||||
|
|
||||||
`ebox-region-ids` 会从未渲染树或已渲染字符串按文档顺序返回 region id。需要
|
|
||||||
低层 source 映射时,在第一次 mount 前捕获:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(let* ((root (ebox-column
|
|
||||||
(ebox-create :id "title" :content "Title")
|
|
||||||
(ebox-create :id "body" :content "Body")))
|
|
||||||
(ids (ebox-region-ids root)))
|
|
||||||
(ebox-render-to-buffer "*Ebox Demo*" root)
|
|
||||||
ids)
|
|
||||||
```
|
|
||||||
|
|
||||||
应用更新优先使用 logical id 与不透明 handle:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(let ((handle (ebox-region-resolve "*Ebox Demo*" "body")))
|
|
||||||
(ebox-region-update handle :content "Changed" :color "#166534"))
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-region-resolve` 在 buffer 不存在、logical id 缺失或 id 有歧义时发出
|
|
||||||
错误。Handle 绑定一个 retained TP surface;对象移除或 buffer kill 后会 stale。
|
|
||||||
数字 region id 是渲染 metadata,不能作为 `ebox-region-update` 的参数。
|
|
||||||
|
|
||||||
### Direct 与 selector 更新
|
|
||||||
|
|
||||||
`ebox-region-update` 接受可变 content/style/scroll property 关键字;除非处于
|
|
||||||
显式 batch,否则返回已提交的 update report。它先构造隔离 candidate,规划
|
|
||||||
最小安全 Ebox owner,再请求 TP 发布一次。它不会添加 child,也不会修改
|
|
||||||
`:id`、`:class`、`:key` 或其他 selector metadata。非法 declaration、render
|
|
||||||
失败、publication 失败或 callback 失败都会保留原 buffer/runtime/report。
|
|
||||||
|
|
||||||
`ebox-selector-update-buffer` 把 property 应用到 buffer 中所有可编辑的
|
|
||||||
selector match,返回:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(:selector SELECTOR
|
|
||||||
:matched INTEGER
|
|
||||||
:updated INTEGER
|
|
||||||
:skipped ((:node-id ID :reason no-region) ...)
|
|
||||||
:reports (REPORT ...))
|
|
||||||
```
|
|
||||||
|
|
||||||
显式多更新 batch 只有在应用确实拥有该边界时才使用较低层 incremental batch
|
|
||||||
函数;一般场景使用 `ebox-selector-update-buffer`。
|
|
||||||
|
|
||||||
### Logical candidate
|
|
||||||
|
|
||||||
Candidate 是基于当前已发布 runtime 的一次性 transaction。多个稳定 subtree
|
|
||||||
需要一起替换并只提交一次时使用:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(let* ((buffer (get-buffer "*Ebox Demo*"))
|
|
||||||
(candidate (ebox-candidate-begin buffer))
|
|
||||||
(match (car (ebox-selector-query-buffer buffer "#body"))))
|
|
||||||
(ebox-candidate-replace
|
|
||||||
candidate
|
|
||||||
(plist-get match :node-id)
|
|
||||||
(ebox-create :id "body" :content "Candidate replacement"))
|
|
||||||
(ebox-commit buffer candidate))
|
|
||||||
```
|
|
||||||
|
|
||||||
如果框架的 semantic owner 与 backend anchor 分离,可在选择更宽的发布 owner
|
|
||||||
之前使用只读函数 `ebox-range-ref-present-p`。当 semantic Range 嵌套在 material
|
|
||||||
anchor 内、当前 Ebox publication 没有独立地址时,它返回 nil。
|
|
||||||
|
|
||||||
`ebox-candidate-replace-host-ref` 使用应用拥有的 `:host-ref`,不需要 runtime
|
|
||||||
node id。两个 replacement 函数都接受可选的 `old-semantic-key` 与
|
|
||||||
`new-semantic-key`,用于有限的 detached identity 复用。Candidate 在 commit
|
|
||||||
时 sealed,不能复用;若捕获的 runtime 或 buffer tick 发生变化,它也会 stale。
|
|
||||||
|
|
||||||
`ebox-candidate-patch-host-paint` 比较前后 declarative Host 输出;只有全部变化
|
|
||||||
都属于 node-local paint 时才记录属性 patch。它保留子树与 runtime identity,
|
|
||||||
并在一次受影响子树遍历中更新继承前景;需要普通子树替换时返回 nil。
|
|
||||||
|
|
||||||
`ebox-candidate-replace-root` 使用私有且绑定 candidate 的 root 地址。它只接受
|
|
||||||
一个声明式节点、清除调用方 runtime identity,并采用 last-wins 语义。最终 root
|
|
||||||
replacement 会吸收其前后记录的 descendant node/host-ref operation,不添加
|
|
||||||
wrapper,也不暴露公共 reference。
|
|
||||||
|
|
||||||
`ebox-child-range` 在 material child list 中创建非节点 segment descriptor。
|
|
||||||
其非 nil ref 在 root 内全局唯一,items 直接参与 parent 的 key、selector、style
|
|
||||||
与 layout scope。`ebox-candidate-replace-range-ref` 接受 proper declarative
|
|
||||||
node list,只替换该 base payload;empty payload 仍可寻址。
|
|
||||||
|
|
||||||
### Host-reference 位置
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-host-ref-position "*Ebox Demo*" 'toolbar)
|
|
||||||
;; => 第一个 live 字符位置,或 nil
|
|
||||||
(ebox-host-ref-bounds "*Ebox Demo*" 'toolbar)
|
|
||||||
;; => (START . END),不含 margin,或 nil
|
|
||||||
```
|
|
||||||
|
|
||||||
位置与 bounds 属于当前 publication generation;每次更新后都要重新获取。
|
|
||||||
Host-reference table 与 logical selector id 是两套不同的机制。
|
|
||||||
|
|
||||||
### Report 与 viewport reflow
|
|
||||||
|
|
||||||
如果上层框架在调用 Ebox 前需要先分配新的 view tree,可以把整个输入 callback
|
|
||||||
纳入 Ebox 的 render GC budget:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-call-with-render-burst
|
|
||||||
(lambda ()
|
|
||||||
(let ((next-root (build-framework-view)))
|
|
||||||
(ebox-commit buffer next-root))))
|
|
||||||
```
|
|
||||||
|
|
||||||
优先使用能保证异常清理的 `ebox-call-with-render-burst`。如果操作无法表达为
|
|
||||||
一次函数调用,`ebox-render-burst-begin` 会返回交给
|
|
||||||
`ebox-render-burst-end` 的不透明 token;调用方必须使用 `unwind-protect`,并按
|
|
||||||
后进先出顺序结束嵌套 token。交互 burst 与 Ebox commit 共用 deferred state,
|
|
||||||
在最外层 end 后 0.2 秒恢复调用方 GC 设置;新 burst 会取消并取代待执行的
|
|
||||||
restore。Batch burst 立即精确恢复 threshold 与 percentage,且不会在边界主动
|
|
||||||
执行 GC。这些函数只定义 allocation/GC ownership,不会 publish、commit,也
|
|
||||||
不会暴露 Ebox runtime state。
|
|
||||||
|
|
||||||
`ebox-buffer-update-report` 返回 mounted buffer 上一次成功报告的 defensive
|
|
||||||
copy;目标缺失或不是 Ebox runtime 时会报错。首次 mount 在第一次 update
|
|
||||||
前没有 update report。报告包含 Ebox strategy、publication scope、TP surface
|
|
||||||
revision、物理 operation 和 reconciliation 事实。
|
|
||||||
|
|
||||||
完整签名是 `(ebox-commit BUFFER NEXT-ROOT &optional FRAMEWORK-PUBLISH
|
|
||||||
FRAMEWORK-ROLLBACK)`。buffer、TP surface 与 Ebox runtime 一致后,publish 接收
|
|
||||||
report;若后续 phase 失败,rollback 最多一次接收同一个 report,其 error/quit
|
|
||||||
会被隔离。`:framework-participant-state`、
|
|
||||||
`:framework-participant-diagnostics` 与 `:scroll-finalization-diagnostics`
|
|
||||||
是只读 outcome 字段。
|
|
||||||
|
|
||||||
当 root 依赖新的 viewport 时使用 `ebox-rerender-buffer-with-context`:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-rerender-buffer-with-context
|
|
||||||
(get-buffer "*Ebox Demo*")
|
|
||||||
800 ; viewport 像素宽度
|
|
||||||
30) ; 可选 viewport 行高
|
|
||||||
```
|
|
||||||
|
|
||||||
该调用保留 node/region identity,并通过 viewport dirty planning 处理变化。
|
|
||||||
`(viewport)` 与 `(viewport-height)` 从该 context 解析。纯渲染时也可以在
|
|
||||||
render 外部动态绑定 `ebox-viewport-width`(像素)与 `ebox-viewport-height`
|
|
||||||
(行数)。不要原地修改已经发布的 node tree。
|
|
||||||
|
|
||||||
可见的 mounted buffer 会自动跟随展示它的 window。Ebox 通过 retained 增量路径
|
|
||||||
立即发布每个 size event。Emacs hook 串行执行并拒绝重入;这里没有第二套宿主
|
|
||||||
hook、timer 或 viewport queue。
|
|
||||||
`ebox-viewport-window-width` 返回 controller 使用的同一个显示安全像素宽度;
|
|
||||||
宿主不应再安装第二套 resize hook 或宽度算法。
|
|
||||||
旧的 `ebox-viewport-resize-delay` 已删除;立即发布不再需要迁移 timer 配置。
|
|
||||||
|
|
||||||
## 7. Selector
|
|
||||||
|
|
||||||
`ebox-selector-parse` 委托 ECSS 并返回结构化 AST;
|
|
||||||
`ebox-selector-match-node-p` 匹配单个声明式节点;`ebox-selector-query-all`
|
|
||||||
从未 mounted tree 返回按文档顺序的 match;`ebox-selector-query-buffer` 查询
|
|
||||||
live buffer 的 retained runtime,并给每条 match 添加 `:buffer`,可编辑节点
|
|
||||||
还会添加 `:region-handle`。
|
|
||||||
|
|
||||||
ECSS 支持的 selector 语法包括:
|
|
||||||
|
|
||||||
- selector list、compound type/id/class selector;
|
|
||||||
- attribute presence 与 `=`、`~=`、`|=`、`^=`、`$=`、`*=`,以及 `i`/`s` flag;
|
|
||||||
- 通过 `:selector-state` 提供的 `:hover`、`:active` 等 state pseudo;
|
|
||||||
- `:is(...)`、`:where(...)`、`:not(...)` 与 relational `:has(...)`;
|
|
||||||
- 空格 descendant、子节点 `>`、相邻兄弟 `+`、一般兄弟 `~` combinator。
|
|
||||||
|
|
||||||
Ebox 的 node type 会映射为 selector type:`box`、`row`、`column`、`flex`、
|
|
||||||
`grid`、`item`。`:id` 与 `:key` 是内建 attribute;额外 attribute 必须显式
|
|
||||||
传入,例如 `:selector-attributes '((role . button))`。可见 content、布局
|
|
||||||
值和内部 runtime slot 不会被推断成 selector attribute。
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-selector-query-buffer "*Ebox Demo*" ".toolbar > box.action")
|
|
||||||
(ebox-selector-query-buffer "*Ebox Demo*" "div:has(> .warning)")
|
|
||||||
(ebox-selector-update-buffer "*Ebox Demo*" "[role=button]"
|
|
||||||
:color "#2563EB")
|
|
||||||
```
|
|
||||||
|
|
||||||
兼容别名 `ebox-select-all` 与 `ebox-update-selector` 分别指向
|
|
||||||
`ebox-selector-query-buffer` 和 `ebox-selector-update-buffer`。
|
|
||||||
|
|
||||||
## 8. 滚动与 viewport state
|
|
||||||
|
|
||||||
设置 `:overflow 'scroll`(默认值)和有限的 `:height` 创建滚动窗口:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-create
|
|
||||||
:id "log"
|
|
||||||
:width '(420)
|
|
||||||
:height 8
|
|
||||||
:overflow 'scroll
|
|
||||||
:content (mapconcat #'identity lines "\n"))
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-scroll-down`、`ebox-scroll-up`、`ebox-scroll-page-down` 和
|
|
||||||
`ebox-scroll-page-up` 操作 point 所在的最内层滚动区域;找不到可消费该命令
|
|
||||||
的 Ebox region 时委托普通 Emacs scrolling。`ebox-wheel-scroll-down` 与
|
|
||||||
`ebox-wheel-scroll-up` 接受 mouse event;当 `ebox-wheel-smooth-scroll` 非 nil
|
|
||||||
时会分步动画,否则委托 `mwheel-scroll`。`ebox-buffer-mode` 在本地安装
|
|
||||||
`ebox-scroll-map`;`ebox-render-to-buffer` 会在返回的 buffer 上启用该 mode。
|
|
||||||
|
|
||||||
`ebox-scroll-state` 接受数字 region id,返回用于 source-model 映射的只读
|
|
||||||
state plist。可用事实包括 `:scroll-offset`、content height/lines 与缓存的
|
|
||||||
visible lines。把 plist 当作 diagnostic/read-only 数据;用
|
|
||||||
`ebox-region-update` 的 `:scroll-offset` 或滚动命令改变位置。
|
|
||||||
|
|
||||||
Scroll content 会惰性预取,并按有界 slice 渲染。安全的 retained update 会
|
|
||||||
保留缓存 scroll state;发布失败时它与其他 runtime 状态一起 rollback。
|
|
||||||
|
|
||||||
## 9. 测量与缓存配置
|
|
||||||
|
|
||||||
`ebox-string-pixel-width` 返回字符串第一行的 display 像素宽度,同时考虑
|
|
||||||
text scale 和固定 `display` space;它也是布局使用的公共测量原语。
|
|
||||||
`ebox-display-signature` 返回测量缓存使用的当前 display 输入。
|
|
||||||
如果在正常 display-signature 信号之外修改了字体或 named face,可调用
|
|
||||||
`ebox-clear-cache`。
|
|
||||||
|
|
||||||
主要 customization 变量如下:
|
|
||||||
|
|
||||||
| 分组 | 变量与默认值 |
|
|
||||||
| --- | --- |
|
|
||||||
| Render cache | `ebox-render-cache-max-entries` 2048;`ebox-render-cache-max-bytes` 32 MiB;`ebox-render-root-cache-max-entries` 16;`ebox-render-root-cache-max-bytes` 8 MiB |
|
|
||||||
| 键盘/鼠标滚动 | `ebox-scroll-step` 1;`ebox-wheel-scroll-step` 1;`ebox-wheel-smooth-scroll` nil;`ebox-wheel-smooth-scroll-interval` 0.016;`ebox-wheel-smooth-scroll-lines-per-tick` 4;`ebox-wheel-smooth-scroll-target-ticks` 8。键盘以 point 为 anchor,滚轮以 event 位置为 anchor,剩余行沿嵌套 owner 向外冒泡。交互式、内容完整且无 chrome 的根 document owner 可在 idle 时一次 materialize,之后使用 Emacs 原生 window line start;普通更新/resize 只预热 retained index,不启动第二次 native publication;嵌套/lazy/chrome owner 继续走事务化 Ebox publication。 |
|
|
||||||
| Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8;`ebox-scroll-lazy-idle-prefetch-lines` 128;`ebox-scroll-lazy-idle-prefetch-slice-lines` 16;`ebox-scroll-lazy-idle-prefetch-delay` 0.15 |
|
|
||||||
| Runtime prewarm | `ebox-runtime-idle-prewarm` t;`ebox-runtime-idle-prewarm-delay` 0.1;`ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0;`ebox-runtime-idle-prewarm-slice-size` 32;`ebox-native-buffer-scroll` t(仅 initial/visible-window handoff;严格 root-owner proof) |
|
|
||||||
| 预测 reflow | `ebox-runtime-idle-reflow-cache-prewarm` t;`ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 |
|
|
||||||
| Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto`;`ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2;`ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB;`ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB;`ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB;`ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24;`ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 |
|
|
||||||
| Visual verification | `ebox-visual-check-output-dir` 指向可选 screenshot/report 使用的临时目录。 |
|
|
||||||
|
|
||||||
Idle prewarm 不会发布 buffer text 或 runtime state。宿主应用不希望后台准备
|
|
||||||
工作时,可把对应的 boolean 设为 nil。
|
|
||||||
|
|
||||||
## 10. 独立 `.ebox` DSL
|
|
||||||
|
|
||||||
`ebox-build` 编译数据型 list form。支持的 tag 是 `box`/`ebox`、`row`、
|
|
||||||
`column`、`flex`、`item`、`grid` 和 `spacer`:
|
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-build
|
(ebox-build
|
||||||
'(grid :width (640)
|
'(column :padding (1 2)
|
||||||
:grid-template-columns ((200) 1fr 1fr)
|
(box :id "status" :color "#166534" "Ready")
|
||||||
:gap (1 (12))
|
(row :item-gap 1
|
||||||
(box :grid-column (1 :span 3) :content "Header")
|
(box "Left")
|
||||||
(box :content "Main")))
|
(box "Right"))))
|
||||||
```
|
```
|
||||||
|
|
||||||
`box` 下的直接字符串会合并为换行内容;child form 会变成默认纵向 child
|
author source field 是 `:key`、`:class` 与 `:id`。`:outer` 可取 `inline` 或
|
||||||
layout。`row` 与 `column` 可以接收 box property 并包住 child layout;`flex`
|
`block`,控制 Box 如何参与父布局。
|
||||||
接收容器 property;`item` 接收 flex item property;`spacer` 不能有 child。
|
|
||||||
|
|
||||||
在 `.ebox` 文件中,结构 form 保持不加 quote;property 位置的 list 和 symbol
|
### 布局拥有的 property
|
||||||
常量要作为 data quote,例如 `:gap '(1 (12))` 或 `:justify-content 'center`;
|
|
||||||
要执行的 Elisp property expression 保持不 quote,交给同级 playground runner
|
|
||||||
求值。
|
|
||||||
|
|
||||||
## 11. 可选 Rust reflow
|
| Form 或关系 | Property |
|
||||||
|
| --- | --- |
|
||||||
|
| `row`、`column` | `:item-gap`、`:cross-align` |
|
||||||
|
| `flex` | `:flex-direction`、`:flex-wrap`、`:flex-flow`、`:justify-content`、`:align-items`、`:align-content`、`:gap`、`:row-gap`、`:column-gap` |
|
||||||
|
| `flex` 的直接子 Box | `:order`、`:flex`、`:flex-grow`、`:flex-shrink`、`:flex-basis`、`:align-self` |
|
||||||
|
| `grid` | `:grid-template-columns`、`:grid-template-rows`、`:grid-auto-columns`、`:grid-auto-rows`、`:grid-auto-flow`、`:justify-items`、`:justify-content`、`:align-items`、`:align-content`、`:gap`、`:row-gap`、`:column-gap` |
|
||||||
|
| `grid` 的直接子 Box | `:order`、`:grid-column`、`:grid-row`、`:grid-column-span`、`:grid-row-span`、`:align-self` |
|
||||||
|
|
||||||
Native 模块是加速器,不是正确性的依赖。Rust 边界只接收有界的数值布局工作;
|
participation property 就是普通子 Box property,不会创建另一种公共节点或 wrapper。
|
||||||
Ebox 保留完全等价的 Elisp fallback,加载时绝不构建 Rust。
|
|
||||||
|
### 当前常用 property
|
||||||
|
|
||||||
|
| 范围 | 当前输入 |
|
||||||
|
| --- | --- |
|
||||||
|
| 尺寸 | `:box-sizing`、`:width`、`:min-width`、`:max-width`、`:height`、`:min-height`、`:max-height` |
|
||||||
|
| 边缘 | `:padding`、`:margin`、`:border`、各 side shorthand 与 longhand |
|
||||||
|
| 绘制 | `:color`、`:background-color`、精确 alias `:bgcolor`、border color、`:visibility` |
|
||||||
|
| 文本布局 | `:text-align`、`:wrap-mode`(`word`、`char`、`kp`、`none`) |
|
||||||
|
| Overflow | `:overflow`(`visible`、`hidden`、`scroll`) |
|
||||||
|
|
||||||
|
普通横向数字表示字符列,单元素横向 list 表示像素;纵向数字表示行数。
|
||||||
|
`(viewport)` 与 `(viewport-height)` 使用当前 render context。property 验证属于
|
||||||
|
Ebox;context/value 组合无效时会直接报错。
|
||||||
|
|
||||||
|
## 2. Evaluated typed construction
|
||||||
|
|
||||||
|
已经拥有 author parsing 的框架可以直接构造 canonical node:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-native-status) ; 显示 toolchain、路径、ABI 与加载诊断
|
(let ((layout (ebox-column-layout-create
|
||||||
(ebox-native-build) ; 异步构建/安装
|
:item-gap 1 :cross-align 'stretch)))
|
||||||
(ebox-native-build t) ; 先清理 Ebox 私有 Cargo cache
|
(ebox-box-create
|
||||||
|
:layout layout
|
||||||
|
:children (list (ebox-text-create :value "One")
|
||||||
|
(ebox-text-create :value "Two"))))
|
||||||
```
|
```
|
||||||
|
|
||||||
仓库命令是 `make native-build`。使用 `ebox-native-reflow-module-path` 配置
|
- `ebox-text-create` 要求 `:value STRING`;
|
||||||
module 文件或目录。Session 限制为 `ebox-native-reflow-max-jobs` 512、
|
- `ebox-normal-layout-create` 构造 Normal LayoutConfig;
|
||||||
`ebox-native-reflow-max-results` 512、`ebox-native-reflow-max-result-bytes`
|
- `ebox-row-layout-create` 与 `ebox-column-layout-create` 接受
|
||||||
64 MiB。模块缺失或 ABI 不兼容时继续使用 Elisp;原因可由
|
`:item-gap` 和 `:cross-align`;
|
||||||
`ebox-native-status` 查看。
|
- `ebox-flex-layout-create` 构造经过验证的 Flex LayoutConfig;
|
||||||
|
- `ebox-grid-layout-create` 构造经过验证的 Grid LayoutConfig;
|
||||||
|
- `ebox-box-create` 要求一个 typed `:layout` 值,并接受 typed node 的
|
||||||
|
`:children` list。
|
||||||
|
|
||||||
`ebox-byte-compile` 会重新编译所有 active Ebox Elisp 源码,但不会构建 Rust。
|
最后两个 field 只属于 evaluated 集成契约,不是 author property,也不是另一套 DSL。
|
||||||
成功后如需当前进程加载新的 `.elc`,请重启 Emacs。
|
|
||||||
|
|
||||||
## 12. 公共函数清单
|
typed 模块还公开 `ebox-node-kind`、`ebox-text-node-p`、`ebox-box-node-p`、
|
||||||
|
`ebox-node-source-handle`、`ebox-text-node-value`、`ebox-box-node-layout`、
|
||||||
|
`ebox-box-node-children` 和 `ebox-box-node-range-anchors`。下面的 facade 清单重点列出
|
||||||
|
应用入口,因此不重复这些 inspection function。
|
||||||
|
|
||||||
下表覆盖门面清单中的公共函数;其下列出的 style-rule 函数是模块级公共
|
## 3. 渲染与 retained 发布
|
||||||
style API。
|
|
||||||
|
|
||||||
| 函数 | 用途 |
|
| 函数 | 契约 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `ebox-text-create`、`ebox-normal-layout-create`、`ebox-row-layout-create`、`ebox-column-layout-create`、`ebox-flex-layout-create`、`ebox-grid-layout-create`、`ebox-box-create` | 框架集成使用的 typed TextNode、Normal/Row/Column/Flex/Grid LayoutConfig 与 BoxNode 端口。 |
|
| `ebox-render` | 返回带属性字符串,不发布 live buffer。 |
|
||||||
| `ebox-create`、`ebox-build` | 迁移期间创建旧节点或编译 `.ebox` list DSL。 |
|
| `ebox-render-to-buffer` | 复制 built root,挂载 retained TP surface,启用 `ebox-buffer-mode` 并返回 buffer。 |
|
||||||
| `ebox-concat`、`ebox-stack`、`ebox-row`、`ebox-column`、`ebox-spacer` | 组合简单横向/纵向布局与空白 box。 |
|
| `ebox-display-buffer` | 走 retained 路径渲染,删除其他 window 后切换到结果。 |
|
||||||
| `ebox-flex`、`ebox-flex-item` | 构建 Flex 容器与 item metadata。 |
|
| `ebox-commit` | 原子发布重新构建的 root 或 one-shot logical candidate。 |
|
||||||
| `ebox-grid`、`ebox-grid-fr` | 构建 Grid 与分数轨道;placement metadata 直接属于 child Box。 |
|
| `ebox-buffer-update-report` | 返回最近一次成功更新报告的防御性副本。 |
|
||||||
| `ebox-render`、`ebox-render-to-buffer`、`ebox-display-buffer` | 纯 materialization、retained mount 与展示包装。 |
|
| `ebox-rerender-buffer-with-context` | 应用显式 viewport width 与可选 height,同时保留 identity。 |
|
||||||
| `ebox-commit`、`ebox-buffer-update-report`、`ebox-rerender-buffer-with-context`、`ebox-viewport-window-width` | 原子根提交、报告查询、viewport-context rerender 与统一的显示安全宽度采样。 |
|
| `ebox-viewport-window-width` | 返回 Ebox 对 window 使用的 display-safe 像素宽度。 |
|
||||||
| `ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update` | Region 映射、logical handle 查询与直接更新。 |
|
|
||||||
| `ebox-child-range`、`ebox-range-ref-present-p`、`ebox-candidate-begin`、`ebox-candidate-replace`、`ebox-candidate-replace-range-ref`、`ebox-candidate-replace-root`、`ebox-candidate-replace-host-ref`、`ebox-candidate-patch-host-paint` | 持久 material child segment、backend anchor 查询、子树替换与 retained Host paint transaction。 |
|
|
||||||
| `ebox-host-ref-bounds`、`ebox-host-ref-position` | Host-reference 的 live bounds 与首位置。 |
|
|
||||||
| `ebox-selector-parse`、`ebox-selector-match-node-p`、`ebox-selector-query-all`、`ebox-selector-query-buffer`、`ebox-selector-update-buffer` | ECSS selector 编译、匹配、查询与批量更新。 |
|
|
||||||
| `ebox-select-all`、`ebox-update-selector` | 两个 buffer selector 函数的兼容别名。 |
|
|
||||||
| `ebox-buffer-mode`、`ebox-scroll-map`、`ebox-scroll-down`、`ebox-scroll-up`、`ebox-scroll-page-down`、`ebox-scroll-page-up`、`ebox-wheel-scroll-down`、`ebox-wheel-scroll-up`、`ebox-scroll-state` | 交互滚动、本地 keymap 与只读滚动状态。 |
|
|
||||||
| `ebox-string-pixel-width`、`ebox-display-signature`、`ebox-clear-cache` | Display-aware 测量与缓存管理。 |
|
|
||||||
| `ebox-native-build`、`ebox-native-status`、`ebox-byte-compile` | 可选 native 工作流与 Elisp byte compile。 |
|
|
||||||
|
|
||||||
加载 `(require 'ebox)` 后可用的 style inspection/rule 入口还包括
|
`ebox-commit` 接受可选 framework publish/rollback callback。它们加入现有原子发布;
|
||||||
`ebox-style-add-rule`、`ebox-style-reset-rules`、
|
失败会恢复旧 buffer、surface、runtime state 与最近一次成功报告。
|
||||||
`ebox-style-cascade-active-p`、`ebox-style-compute`、
|
|
||||||
`ebox-style-expand-shorthands`、`ebox-style-compile-declarations`、
|
|
||||||
`ebox-style-merge-declarations`、`ebox-style-compute-subject`、
|
|
||||||
`ebox-style-property`、`ebox-style-canonical-name` 和
|
|
||||||
`ebox-style-dirty-kind`。前两个是应用通常使用的入口;其余函数供需要检查
|
|
||||||
或准备 ECSS-backed style data 的集成使用。
|
|
||||||
|
|
||||||
## 13. 验证与支持边界
|
`ebox-call-with-render-burst` 是框架 callback 的 exception-safe allocation/GC 边界。
|
||||||
|
`ebox-render-burst-begin` 与 `ebox-render-burst-end` 提供较底层的 token 形式,必须用
|
||||||
|
`unwind-protect` 配对。它们本身不发布内容。
|
||||||
|
|
||||||
在仓库根目录运行:
|
## 4. Identity、直接更新与 logical candidate
|
||||||
|
|
||||||
|
| 函数 | 契约 |
|
||||||
|
| --- | --- |
|
||||||
|
| `ebox-region-ids` | 按文档顺序返回当前底层 region id。 |
|
||||||
|
| `ebox-region-resolve` | 把 mounted buffer 中一个逻辑 `:id` 解析为不透明、surface-scoped 的 handle。 |
|
||||||
|
| `ebox-region-update` | 通过一次 retained transaction 应用支持的可变样式或滚动 property。 |
|
||||||
|
| `ebox-child-range` | 从 typed node 构造透明、可寻址的 child Range descriptor。 |
|
||||||
|
| `ebox-candidate-begin` | 在 one-shot candidate 中捕获当前准确 mounted generation。 |
|
||||||
|
| `ebox-candidate-replace` | 替换一个 candidate node address。 |
|
||||||
|
| `ebox-candidate-replace-range-ref` | 替换一个透明 child Range payload。 |
|
||||||
|
| `ebox-candidate-replace-root` | 替换 candidate 的私有 root address。 |
|
||||||
|
| `ebox-candidate-replace-host-ref` | 替换一个 framework-owned opaque host reference。 |
|
||||||
|
| `ebox-candidate-patch-host-paint` | 记录经过证明的 paint-only host patch;需要 replacement 时返回 nil。 |
|
||||||
|
| `ebox-host-ref-bounds` | 返回 opaque host reference 当前不含 margin 的 bounds。 |
|
||||||
|
| `ebox-host-ref-position` | 返回其当前第一个位置。 |
|
||||||
|
|
||||||
|
handle 和位置只属于一个 live publication generation;对象被删除后要重新解析。
|
||||||
|
candidate 被 commit 封存后不能复用。
|
||||||
|
|
||||||
|
## 5. Selector
|
||||||
|
|
||||||
|
ECSS 是唯一 selector parser 与 matcher;Ebox 只提供 logical tree relation 和
|
||||||
|
mounted index。
|
||||||
|
|
||||||
|
| 函数 | 契约 |
|
||||||
|
| --- | --- |
|
||||||
|
| `ebox-selector-parse` | 把 CSS-like selector 编译为 ECSS AST。 |
|
||||||
|
| `ebox-selector-match-node-p` | 用 selector 或 AST 匹配一个 built node。 |
|
||||||
|
| `ebox-selector-query-all` | 按文档顺序查询 unmounted built tree。 |
|
||||||
|
| `ebox-selector-query-buffer` | 查询一个 mounted runtime。 |
|
||||||
|
| `ebox-selector-update-buffer` | 更新所有可编辑 mounted match 并返回结构化报告。 |
|
||||||
|
| `ebox-select-all` | `ebox-selector-query-buffer` 的 alias。 |
|
||||||
|
| `ebox-update-selector` | `ebox-selector-update-buffer` 的 alias。 |
|
||||||
|
|
||||||
|
## 6. 滚动
|
||||||
|
|
||||||
|
`ebox-buffer-mode` 安装 `ebox-scroll-map`。map 会把行、页和滚轮意图先交给最内层
|
||||||
|
semantic scroll owner,再回退到普通 Emacs 滚动。
|
||||||
|
|
||||||
|
公共命令是 `ebox-scroll-down`、`ebox-scroll-up`、`ebox-scroll-page-down`、
|
||||||
|
`ebox-scroll-page-up`、`ebox-wheel-scroll-down` 与 `ebox-wheel-scroll-up`。
|
||||||
|
`ebox-scroll-state` 根据数字 region id 返回只读快照。
|
||||||
|
|
||||||
|
## 7. 测量、缓存与构建工具
|
||||||
|
|
||||||
|
| 函数 | 契约 |
|
||||||
|
| --- | --- |
|
||||||
|
| `ebox-string-pixel-width` | 测量字符串第一行的 display width。 |
|
||||||
|
| `ebox-display-signature` | 返回 measurement cache invalidation 使用的 display 输入。 |
|
||||||
|
| `ebox-clear-cache` | 清理 Ebox measurement/render cache。 |
|
||||||
|
| `ebox-byte-compile` | 编译 Ebox 活跃 Elisp 源码;重启 Emacs 后加载新 bytecode。 |
|
||||||
|
| `ebox-native-status` | 报告 native toolchain、module、ABI 与加载状态。 |
|
||||||
|
| `ebox-native-build` | 异步构建并安装可选 native 模块。 |
|
||||||
|
|
||||||
|
native 模块只是加速器,不是正确性依赖。加载包不会构建它。
|
||||||
|
|
||||||
|
## 8. Facade 清单
|
||||||
|
|
||||||
|
`ebox-public-api` 保存稳定 facade 入口。下面的分组列出其中每一个名称:
|
||||||
|
|
||||||
|
- 构造:`ebox-build`、`ebox-text-create`、`ebox-normal-layout-create`、
|
||||||
|
`ebox-row-layout-create`、`ebox-column-layout-create`、
|
||||||
|
`ebox-flex-layout-create`、`ebox-grid-layout-create`、`ebox-box-create`、
|
||||||
|
`ebox-child-range`;
|
||||||
|
- 渲染/发布:`ebox-render`、`ebox-render-to-buffer`、`ebox-display-buffer`、
|
||||||
|
`ebox-commit`、`ebox-buffer-update-report`、
|
||||||
|
`ebox-rerender-buffer-with-context`、`ebox-viewport-window-width`、
|
||||||
|
`ebox-call-with-render-burst`、`ebox-render-burst-begin`、
|
||||||
|
`ebox-render-burst-end`;
|
||||||
|
- Candidate/identity:`ebox-candidate-begin`、`ebox-candidate-replace`、
|
||||||
|
`ebox-candidate-replace-range-ref`、`ebox-candidate-replace-root`、
|
||||||
|
`ebox-candidate-replace-host-ref`、`ebox-candidate-patch-host-paint`、
|
||||||
|
`ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update`、
|
||||||
|
`ebox-host-ref-bounds`、`ebox-host-ref-position`;
|
||||||
|
- Selector:`ebox-selector-parse`、`ebox-selector-match-node-p`、
|
||||||
|
`ebox-selector-query-all`、`ebox-selector-query-buffer`、
|
||||||
|
`ebox-selector-update-buffer`、`ebox-select-all`、`ebox-update-selector`;
|
||||||
|
- 滚动:`ebox-buffer-mode`、`ebox-scroll-map`、`ebox-scroll-down`、
|
||||||
|
`ebox-scroll-up`、`ebox-scroll-page-down`、`ebox-scroll-page-up`、
|
||||||
|
`ebox-wheel-scroll-down`、`ebox-wheel-scroll-up`、`ebox-scroll-state`;
|
||||||
|
- 测量/构建:`ebox-string-pixel-width`、`ebox-display-signature`、
|
||||||
|
`ebox-clear-cache`、`ebox-byte-compile`、`ebox-native-status`、
|
||||||
|
`ebox-native-build`。
|
||||||
|
|
||||||
|
## 9. 验证
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
|
||||||
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
make native-rust-tests
|
make dsl-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
|
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
```
|
```
|
||||||
|
|
||||||
聚焦目标包括 `make core-tests`、`make grid-tests`、`make surface-tests`、
|
|
||||||
`make selector-tests`、`make dsl-tests`、`make flex-tests` 和
|
|
||||||
`make visual-check-tests`。可运行的 `.ebox` 示例由同级 `ebox-playground`
|
|
||||||
包负责;ETAF 负责 Component、state、control 和应用行为。本包不要调用
|
|
||||||
私有 `ebox--*` 函数,也不要在 Ebox 任务中编辑历史 Ebox checkout。
|
|
||||||
|
|||||||
@ -1,197 +1,230 @@
|
|||||||
# Ebox user guide
|
# Ebox user guide
|
||||||
|
|
||||||
Ebox is the low-level box, layout, and buffer-rendering package. Use it directly when an application needs precise geometry or use it as the rendering substrate below ETAF. This guide intentionally does not introduce Components, reactive state, behaviors, or application data. For the complete function/property/configuration inventory, see the [public API reference](ebox-api-reference.en.md).
|
[中文](ebox-user-guide.zh.md)
|
||||||
|
|
||||||
## 1. Load the package
|
Ebox is the low-level Text/Box layout and buffer-rendering package. This guide
|
||||||
|
uses one public author grammar and keeps framework-integration details separate.
|
||||||
|
See the [API reference](ebox-api-reference.en.md) for the complete function
|
||||||
|
inventory.
|
||||||
|
|
||||||
|
## 1. Load Ebox
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(add-to-list 'load-path "/path/to/github/ebox")
|
|
||||||
(require 'ebox)
|
(require 'ebox)
|
||||||
```
|
```
|
||||||
|
|
||||||
Loading defines the public package and its pure layout modules. It does not create a buffer, install a mode, build Rust code, or change the current editing buffer.
|
Loading Ebox does not create a buffer, enable a mode in the current buffer, or
|
||||||
|
build Rust.
|
||||||
|
|
||||||
## 2. Build a node tree
|
## 2. Learn one author model
|
||||||
|
|
||||||
`ebox-create` makes a node. Container helpers accept child nodes and return another node:
|
An Ebox document contains Text and Box nodes. The author grammar has exactly
|
||||||
|
seven entries:
|
||||||
|
|
||||||
|
| Entry | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `"text"` | Short form for one Text node. |
|
||||||
|
| `(text ... "text")` | Text with explicit text properties. |
|
||||||
|
| `(box ... CHILD...)` | A normal visual Box. |
|
||||||
|
| `(row ... CHILD...)` | A Box with simple horizontal child layout. |
|
||||||
|
| `(column ... CHILD...)` | A Box with simple vertical child layout. |
|
||||||
|
| `(flex ... CHILD...)` | A Box with Flex child layout. |
|
||||||
|
| `(grid ... CHILD...)` | A Box with Grid child layout. |
|
||||||
|
|
||||||
|
Children are always nested directly. A layout form is a Box with a selected
|
||||||
|
child-layout algorithm, not a different kind of visual object.
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-column
|
(defvar ebox-guide-root
|
||||||
(ebox-create :content "Title"
|
(ebox-build
|
||||||
:font 'bold
|
'(column :padding (1 2)
|
||||||
:color "#263244"
|
:border ((1) solid "#8A93A6")
|
||||||
:bgcolor "#F4F6FB"
|
(text :color "#263244" "Research notes")
|
||||||
:padding '(1 2))
|
(row :item-gap 1
|
||||||
(ebox-row
|
(box :id "status" :background-color "#F4F6FB" "Inbox")
|
||||||
(ebox-create :content "Left" :width 12)
|
(box :background-color "#EEF2FF" "Archive")))))
|
||||||
(ebox-create :content "Right" :width 12)))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The public shape is data, not rendered text. A node may contain string
|
Use `box` with geometry but no child when an empty rectangular area is needed.
|
||||||
`:content` or child nodes supplied to a container helper; wrapper bookkeeping
|
No extra node type is necessary.
|
||||||
stays internal. Use `:key` when siblings have stable application identity and
|
|
||||||
`:host-ref` when an application needs a public handle position after rendering.
|
|
||||||
|
|
||||||
## 3. Dimensions and surface properties
|
## 3. Choose the layout that states the intent
|
||||||
|
|
||||||
Ordinary horizontal numbers are character columns. A one-element list denotes a pixel width; vertical numbers are line counts. Padding and margins accept scalar or CSS-like 1–4-value forms. Borders are width, style, and color:
|
Use `box` for ordinary content, `row` or `column` for direct one-axis
|
||||||
|
composition, `flex` when free space or wrapping matters, and `grid` for
|
||||||
|
two-dimensional tracks or explicit placement.
|
||||||
|
|
||||||
|
### Row and column
|
||||||
|
|
||||||
|
`row` and `column` accept `:item-gap` and `:cross-align`. Their children remain
|
||||||
|
ordinary Text or Box nodes.
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-create
|
(ebox-build
|
||||||
:content "A readable panel"
|
'(row :item-gap 2 :cross-align center
|
||||||
:width '(420)
|
(box :width 12 "Left")
|
||||||
:padding '(1 2)
|
(box :width 12 "Right")))
|
||||||
:margin '(0 1)
|
|
||||||
:border '((1) solid "#8A93A6")
|
|
||||||
:color "#263244"
|
|
||||||
:bgcolor "#FFFFFF")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep foreground and background explicit on tinted surfaces. `:font` may be a face symbol or a face plist; use `:color` and `:bgcolor` when the surface itself carries semantic colors. `:font-family`, `:font-height`/`:font-size`, `:font-weight`, and `:font-slant` are the supported typography longhands.
|
### Flex
|
||||||
|
|
||||||
### Stylesheet rules
|
Flex container properties belong to `flex`. Participation properties belong
|
||||||
|
directly to a child `box` because they describe the parent-child relationship.
|
||||||
Inline properties are compiled by Ebox's ECSS-backed style domain. Selector-driven rules use the isolated stylesheet:
|
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-style-reset-rules)
|
(ebox-build
|
||||||
(ebox-style-add-rule ".card"
|
'(flex :width (480)
|
||||||
'(:color "#1F2937" :padding '(1 (12)))
|
:flex-flow (row wrap)
|
||||||
:layer 'base)
|
:gap (1 (12))
|
||||||
|
(box :flex (1 1 auto) "Primary")
|
||||||
|
(box :flex-grow 2 "Secondary")))
|
||||||
```
|
```
|
||||||
|
|
||||||
Rules use ECSS cascade semantics, including `:origin`, `:layer`, and `:scope`. A rule change does not publish an already-mounted buffer automatically; rerender or commit the affected buffer after changing rules. See the [API reference](ebox-api-reference.en.md#5-stylesheets-and-cascade) for the complete property schema.
|
### Grid
|
||||||
|
|
||||||
## 4. Row, column, flex, and Grid
|
Grid placement is one-based. Tracks may be fixed, `auto`, fractional,
|
||||||
|
`minmax`, or `repeat` values. Placement properties also belong directly to a
|
||||||
Use row and column for simple one-dimensional composition. Use flex when free space is distributed among items. Use Grid when two-dimensional tracks or stable placement matter:
|
child `box`.
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-grid
|
|
||||||
:width '(640)
|
|
||||||
:grid-template-columns '((200) 1fr 1fr)
|
|
||||||
:grid-template-rows '(1 1)
|
|
||||||
:gap '(1 (12))
|
|
||||||
:padding '(1 2)
|
|
||||||
:border '((1) solid "#8A93A6")
|
|
||||||
(ebox-create :content "Header" :grid-column 1 :grid-column-span 3)
|
|
||||||
(ebox-create :content "Navigation" :grid-column 1 :grid-row 2)
|
|
||||||
(ebox-create :content "Main" :grid-column 2 :grid-row 2)
|
|
||||||
(ebox-create :content "Aside" :grid-column 3 :grid-row 2))
|
|
||||||
```
|
|
||||||
|
|
||||||
Grid tracks can be fixed, fractional, `auto`, `minmax`, or repeated. Explicit placement is one-based. Use positive integer spans and let implicit tracks fill omitted positions. Multiline items are aligned line by line to their grid rectangle before a wrapper border is painted, keeping a bordered Grid's right edge continuous even when child lines have different natural widths.
|
|
||||||
|
|
||||||
## 5. Render text or a buffer
|
|
||||||
|
|
||||||
`ebox-render` is pure with respect to buffers and returns propertized text through an ephemeral TP surface. `ebox-render-to-buffer` copies the declarative source and mounts a retained TP surface for initial publication:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(let ((node (ebox-column
|
|
||||||
(ebox-create :id "status" :content "Ready" :width '(240))
|
|
||||||
(ebox-create :content "Rendered by Ebox"))))
|
|
||||||
(ebox-render node)
|
|
||||||
(ebox-render-to-buffer "*Ebox Demo*" node))
|
|
||||||
```
|
|
||||||
|
|
||||||
The returned text carries display, face, region, and identity properties needed by Ebox. Do not edit those properties by hand. The source node remains caller-owned and can be mounted in more than one buffer; each buffer receives independent runtime identity and state.
|
|
||||||
|
|
||||||
## 6. Update an existing buffer
|
|
||||||
|
|
||||||
Build a fresh root tree and commit it to the existing buffer:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-commit
|
|
||||||
"*Ebox Demo*"
|
|
||||||
(ebox-column
|
|
||||||
(ebox-create :content "Updated" :key 'title :width '(240))))
|
|
||||||
```
|
|
||||||
|
|
||||||
Ebox compares stable keys and region identity, prepares the semantic dirty/owner plan, and asks TP to atomically publish the new retained surface and Ebox runtime state. It records both the Ebox plan and TP execution summary:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-buffer-update-report "*Ebox Demo*")
|
|
||||||
```
|
|
||||||
|
|
||||||
If a candidate cannot be proven safe, Ebox escalates to an owner or root rerender. A failed render, publication, runtime-state swap, or publication callback leaves the previous buffer, TP surface, Ebox runtime state, and last successful report intact.
|
|
||||||
|
|
||||||
## 7. Selectors and handles
|
|
||||||
|
|
||||||
Selectors query the rendered tree and return public match records. They do not own application state. Give an editable box a logical `:id`, resolve it in one live buffer, and pass the opaque surface-scoped handle to `ebox-region-update`:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(let ((handle (ebox-region-resolve "*Ebox Demo*" "status")))
|
|
||||||
(ebox-region-update handle :content "Ready" :color "#166534"))
|
|
||||||
```
|
|
||||||
|
|
||||||
The same logical id in two buffers resolves to two different handles, so updating one surface cannot accidentally mutate the other. A handle becomes stale when its retained object is removed or its buffer is killed. `ebox-region-update` accepts only a live handle; numeric region ids are internal render metadata and are not an update API.
|
|
||||||
|
|
||||||
`ebox-selector-parse` compiles CSS-like strings directly to ECSS's structured selector AST. Queries support selector lists, type, `#id`, `.class`, attribute presence and `=`, `~=`, `|=`, `^=`, `$=`, `*=` operators, state pseudos, `:is()`, `:where()`, `:not()`, `:has()`, descendant whitespace, child `>`, adjacent sibling `+`, and general sibling `~`. Ebox maps logical children and indexed candidates to ECSS subjects; `ecss-selector-match-p` is the only final matcher, so query and cascade semantics cannot diverge. Attribute selectors see built-in `:id`/`:key` plus metadata explicitly supplied through `:selector-attributes`, for example `:selector-attributes '((role . button))`; visible content, layout properties, Ebox runtime containers, and internal `:ebox-*` slots never become selector attributes implicitly.
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-selector-query-buffer "*Ebox Demo*" ".toolbar > box.action")
|
|
||||||
(ebox-selector-query-buffer "*Ebox Demo*" "#first + .later")
|
|
||||||
(ebox-selector-query-buffer "*Ebox Demo*" "#first ~ [role=button]")
|
|
||||||
(ebox-selector-update-buffer "*Ebox Demo*" ".action" :color "#2563EB")
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-selector-update-buffer` returns `:matched`, `:updated`, structured `:skipped`, and `:reports` fields. The compatibility aliases `ebox-select-all` and `ebox-update-selector` refer to the query and update functions.
|
|
||||||
|
|
||||||
## 8. Scroll and viewport context
|
|
||||||
|
|
||||||
Give a box `:overflow 'scroll` (the default) and a finite `:height` to create a scroll window:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-create :id "log" :width '(420) :height 8
|
|
||||||
:overflow 'scroll
|
|
||||||
:content (mapconcat #'identity lines "\n"))
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-scroll-down`, `ebox-scroll-up`, `ebox-scroll-page-down`, and `ebox-scroll-page-up` start at the innermost scroll owner under point. Keyboard line intent is allocated one line at a time through semantic scroll ancestors; any residual continues to the parent and then ordinary Emacs buffer scrolling. `ebox-wheel-scroll-down` and `ebox-wheel-scroll-up` anchor at the mouse event position (not point) and use the same inner-to-outer residual rule. When no Ebox region can consume the request they delegate to normal Emacs scrolling. `ebox-buffer-mode` installs `ebox-scroll-map` locally; `ebox-render-to-buffer` enables it on its returned buffer. `ebox-scroll-state` exposes read-only scroll facts for a numeric region id; use scroll commands or `ebox-region-update` with `:scroll-offset` to change position.
|
|
||||||
|
|
||||||
Viewport-dependent values use `(viewport)` and `(viewport-height)`. Rerender a mounted buffer with an explicit context:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-rerender-buffer-with-context (get-buffer "*Ebox Demo*") 800 30)
|
|
||||||
```
|
|
||||||
|
|
||||||
The call preserves node and region identity. In an interactive preview, a complete chrome-free root document owner is idle-materialized once and then follows Emacs's native line-oriented window scrolling; nested or still-lazy owners keep the transactional retained-window path. The [API reference](ebox-api-reference.en.md#8-scrolling-and-viewport-state) lists lazy prefix, idle prefetch, cache, and scroll customization variables.
|
|
||||||
|
|
||||||
## 9. Standalone `.ebox` files
|
|
||||||
|
|
||||||
`ebox-build` reads one data-oriented Ebox form:
|
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-build
|
(ebox-build
|
||||||
'(grid :width (640)
|
'(grid :width (640)
|
||||||
:grid-template-columns ((200) 1fr 1fr)
|
:grid-template-columns ((200) 1fr 1fr)
|
||||||
|
:grid-template-rows (1 1)
|
||||||
:gap (1 (12))
|
:gap (1 (12))
|
||||||
(box :content "A")
|
(box :grid-column (1 :span 3) "Header")
|
||||||
(box :content "B")
|
(box :grid-column 1 :grid-row 2 "Navigation")
|
||||||
(box :content "C")))
|
(box :grid-column 2 :grid-row 2 "Main")
|
||||||
|
(box :grid-column 3 :grid-row 2 "Aside")))
|
||||||
```
|
```
|
||||||
|
|
||||||
Inside a `.ebox` fixture, keep the structural form unquoted. Quote list and symbol constants in property positions (for example, `:gap '(1 (12))` and `:justify-content 'center`), while leaving executable Elisp property expressions unquoted. `ebox-playground` evaluates those property expressions before passing the form to `ebox-build`, matching the `etaf-view` value convention.
|
## 4. Use geometry and paint properties
|
||||||
|
|
||||||
Executable `.ebox` references are maintained by the sibling [`ebox-playground`](../../ebox-playground/README.md) package. Its `examples/` directory contains the migrated Basic, Comprehensive, Flex, Responsive, and complete Grid reference files; they remain low-level layout examples and do not require the ETAF framework.
|
Horizontal numbers are character columns; a one-element list such as `(240)`
|
||||||
|
is a pixel width. Vertical numbers are lines. Text accepts only font,
|
||||||
|
foreground/background, and text-decoration properties. Padding, margin,
|
||||||
|
border, size, `:outer`, overflow, visibility, and wrapping policy belong only
|
||||||
|
to Box. Font and color on Box may feed inherited Text facts, but never give
|
||||||
|
Text Box geometry.
|
||||||
|
|
||||||
## 10. Optional native reflow
|
```elisp
|
||||||
|
(ebox-build
|
||||||
|
'(box :width (420)
|
||||||
|
:padding (1 2)
|
||||||
|
:margin (0 1)
|
||||||
|
:border ((1) solid "#8A93A6")
|
||||||
|
:color "#263244"
|
||||||
|
:background-color "#FFFFFF"
|
||||||
|
"A readable panel"))
|
||||||
|
```
|
||||||
|
|
||||||
The Rust module accelerates eligible reflow work; it is not required for correctness. Load Ebox normally, run `make native-build` when you want a local module, and configure `ebox-native-reflow-module-path` if the module is outside its default location. Ebox keeps the Elisp path as the exact fallback and never builds native code while loading. `ebox-native-status` shows toolchain, module, ABI, and load diagnosis; `ebox-native-build` starts an asynchronous build and accepts a prefix argument for a clean private Cargo cache rebuild.
|
Use `:outer inline` or `:outer block` to state how a Box participates in its
|
||||||
|
parent. The child-layout algorithm still comes from the form name.
|
||||||
|
|
||||||
## 11. Public boundary
|
## 5. Render and publish
|
||||||
|
|
||||||
Use the functions and properties listed in the [public API reference](ebox-api-reference.en.md) and the `ebox-public-api` facade inventory. Names beginning with `ebox--` are private implementation details and may change. ETAF is the sibling package for Components, View trees, state, behavior, Context, data, and application lifecycle; Ebox should stay focused on geometry and publication.
|
`ebox-render` returns propertized text without changing a live buffer.
|
||||||
|
`ebox-render-to-buffer` mounts a retained TP surface and returns its buffer.
|
||||||
|
|
||||||
## 12. Verification
|
```elisp
|
||||||
|
(ebox-render ebox-guide-root)
|
||||||
|
(ebox-render-to-buffer "*Ebox Guide*" ebox-guide-root)
|
||||||
|
```
|
||||||
|
|
||||||
|
Build a fresh root and use `ebox-commit` for an atomic update:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-commit
|
||||||
|
"*Ebox Guide*"
|
||||||
|
(ebox-build
|
||||||
|
'(column :padding (1 2)
|
||||||
|
(text "Updated notes")
|
||||||
|
(box :key body "The new root is caller-owned."))))
|
||||||
|
```
|
||||||
|
|
||||||
|
Validation, rendering, or publication failure leaves the previous buffer and
|
||||||
|
runtime state intact. `ebox-buffer-update-report` returns a defensive copy of
|
||||||
|
the last successful update report.
|
||||||
|
|
||||||
|
## 6. Query and update a mounted surface
|
||||||
|
|
||||||
|
`:id`, `:class`, and `:key` are author metadata. Selectors use ECSS semantics.
|
||||||
|
Resolve an `:id` to an opaque, surface-scoped handle before a direct update:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(let ((handle (ebox-region-resolve "*Ebox Guide*" "status")))
|
||||||
|
(ebox-region-update handle :color "#166534"))
|
||||||
|
```
|
||||||
|
|
||||||
|
`ebox-selector-query-buffer` returns document-ordered matches from a mounted
|
||||||
|
buffer. `ebox-selector-update-buffer` applies one style update to all editable
|
||||||
|
matches. Numeric region ids are diagnostic render metadata, not stable update
|
||||||
|
handles.
|
||||||
|
|
||||||
|
## 7. Resize and scroll
|
||||||
|
|
||||||
|
A finite height plus `:overflow scroll` creates a scroll window. Ebox buffer
|
||||||
|
mode installs keyboard and wheel commands that route through the innermost
|
||||||
|
scroll owner before falling back to ordinary Emacs scrolling.
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-build
|
||||||
|
'(box :id log :width (420) :height 8 :overflow scroll
|
||||||
|
"line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9"))
|
||||||
|
```
|
||||||
|
|
||||||
|
Visible mounted buffers follow their display window. Integrations may apply an
|
||||||
|
explicit viewport with:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-rerender-buffer-with-context
|
||||||
|
(get-buffer "*Ebox Guide*") 800 30)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Standalone `.ebox` files
|
||||||
|
|
||||||
|
A `.ebox` file contains one unquoted structural form:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(column :padding '(1 2)
|
||||||
|
(text :color "#263244" "Title")
|
||||||
|
(box :width '(240) "Body"))
|
||||||
|
```
|
||||||
|
|
||||||
|
The sibling `ebox-playground` evaluates property expressions before calling
|
||||||
|
`ebox-build`. Quote list and symbol constants in a `.ebox` file, as shown
|
||||||
|
above. When calling `ebox-build` directly, pass inert data as in the earlier
|
||||||
|
examples.
|
||||||
|
|
||||||
|
## 9. Typed integration API
|
||||||
|
|
||||||
|
Frameworks that already normalize author input may bypass the list DSL. They
|
||||||
|
construct one TextNode with `ebox-text-create`, one typed LayoutConfig with the
|
||||||
|
matching layout constructor, then one BoxNode with `ebox-box-create`.
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-box-create
|
||||||
|
:layout (ebox-row-layout-create :item-gap 1 :cross-align 'center)
|
||||||
|
:children (list (ebox-text-create :value "Left")
|
||||||
|
(ebox-text-create :value "Right")))
|
||||||
|
```
|
||||||
|
|
||||||
|
Here `:layout` and `:children` are fields of the evaluated typed constructor.
|
||||||
|
They are not author properties and do not extend the seven-entry grammar.
|
||||||
|
|
||||||
|
## 10. Optional native reflow and verification
|
||||||
|
|
||||||
|
The Rust module is optional and Ebox never builds it while loading:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-native-status)
|
||||||
|
(ebox-native-build)
|
||||||
|
```
|
||||||
|
|
||||||
From the repository root:
|
From the repository root:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
|
||||||
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
|
||||||
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
|
||||||
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
|
make dsl-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
|
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `make grid-tests`, `make dsl-tests`, `make selector-tests`, `make surface-tests`, or `make visual-check-tests` for focused changes. Use `make native-rust-tests` after changing the native module.
|
|
||||||
|
|||||||
@ -1,197 +1,216 @@
|
|||||||
# Ebox 用户指南
|
# Ebox 用户指南
|
||||||
|
|
||||||
Ebox 是底层的 box、布局和 buffer 渲染包。当应用需要精确几何时可以直接使用,也可以作为 ETAF 下方的渲染基座。本指南不引入 Component、响应式状态、behavior 或应用数据。完整的函数、property、配置清单见[公共 API 参考](ebox-api-reference.zh.md)。
|
[English](ebox-user-guide.en.md)
|
||||||
|
|
||||||
## 1. 加载包
|
Ebox 是底层 Text/Box 布局与 buffer 渲染包。本指南只使用一套公共 author 语法,
|
||||||
|
并把框架集成接口单独说明。完整函数清单见[公共 API
|
||||||
|
参考](ebox-api-reference.zh.md)。
|
||||||
|
|
||||||
|
## 1. 加载 Ebox
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(add-to-list 'load-path "/path/to/github/ebox")
|
|
||||||
(require 'ebox)
|
(require 'ebox)
|
||||||
```
|
```
|
||||||
|
|
||||||
加载只定义公共包和纯布局模块,不会创建 buffer、安装 mode、构建 Rust 或修改当前编辑 buffer。
|
加载 Ebox 不会创建 buffer、在当前 buffer 启用 mode,也不会构建 Rust。
|
||||||
|
|
||||||
## 2. 构造节点树
|
## 2. 只学习一个 author 模型
|
||||||
|
|
||||||
`ebox-create` 构造节点;容器辅助函数接收子节点并返回新的节点:
|
Ebox 文档由 Text 和 Box 节点组成。author 语法只有七个入口:
|
||||||
|
|
||||||
|
| 入口 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `"text"` | 一个 Text 节点的简写。 |
|
||||||
|
| `(text ... "text")` | 带显式文本 property 的 Text。 |
|
||||||
|
| `(box ... CHILD...)` | 普通视觉 Box。 |
|
||||||
|
| `(row ... CHILD...)` | 使用简单横向子布局的 Box。 |
|
||||||
|
| `(column ... CHILD...)` | 使用简单纵向子布局的 Box。 |
|
||||||
|
| `(flex ... CHILD...)` | 使用 Flex 子布局的 Box。 |
|
||||||
|
| `(grid ... CHILD...)` | 使用 Grid 子布局的 Box。 |
|
||||||
|
|
||||||
|
子节点始终直接嵌套。布局 form 只是选择了子布局算法的 Box,不是另一种视觉对象。
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-column
|
(defvar ebox-guide-root
|
||||||
(ebox-create :content "Title"
|
(ebox-build
|
||||||
:font 'bold
|
'(column :padding (1 2)
|
||||||
:color "#263244"
|
:border ((1) solid "#8A93A6")
|
||||||
:bgcolor "#F4F6FB"
|
(text :color "#263244" "Research notes")
|
||||||
:padding '(1 2))
|
(row :item-gap 1
|
||||||
(ebox-row
|
(box :id "status" :background-color "#F4F6FB" "Inbox")
|
||||||
(ebox-create :content "Left" :width 12)
|
(box :background-color "#EEF2FF" "Archive")))))
|
||||||
(ebox-create :content "Right" :width 12)))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
公共形状是数据,而不是已渲染文本。节点可以有字符串 `:content`,或由容器
|
需要空矩形区域时,使用只有几何 property、没有子节点的 `box`,不需要额外节点类型。
|
||||||
辅助函数接收的子节点;wrapper bookkeeping 保持内部。兄弟节点具有稳定业务
|
|
||||||
identity 时使用 `:key`;应用需要在渲染后查询公开位置时使用 `:host-ref`。
|
|
||||||
|
|
||||||
## 3. 尺寸与表面属性
|
## 3. 用布局名称直接表达意图
|
||||||
|
|
||||||
普通横向数字表示字符列;单元素 list 表示像素宽度;纵向数字表示行数。padding 和 margin 接受标量或 CSS 风格的 1–4 值。border 由宽度、样式和颜色组成:
|
普通内容用 `box`;直接的一维组合用 `row` 或 `column`;需要分配剩余空间或换行时
|
||||||
|
用 `flex`;需要二维轨道或显式放置时用 `grid`。
|
||||||
|
|
||||||
|
### Row 与 column
|
||||||
|
|
||||||
|
`row` 和 `column` 接受 `:item-gap` 与 `:cross-align`。它们的子节点仍然只是 Text
|
||||||
|
或 Box。
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-create
|
(ebox-build
|
||||||
:content "A readable panel"
|
'(row :item-gap 2 :cross-align center
|
||||||
:width '(420)
|
(box :width 12 "Left")
|
||||||
:padding '(1 2)
|
(box :width 12 "Right")))
|
||||||
:margin '(0 1)
|
|
||||||
:border '((1) solid "#8A93A6")
|
|
||||||
:color "#263244"
|
|
||||||
:bgcolor "#FFFFFF")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
带色背景的 surface 要显式设置前景色与背景色。`:font` 可以是 face symbol 或 face plist;当 surface 本身承载语义颜色时,优先使用 `:color` 与 `:bgcolor`。支持的 typography longhand 是 `:font-family`、`:font-height`/`:font-size`、`:font-weight` 和 `:font-slant`。
|
### Flex
|
||||||
|
|
||||||
### Stylesheet 规则
|
Flex 容器 property 属于 `flex`。participation property 直接属于子 `box`,因为它
|
||||||
|
描述的是父子关系。
|
||||||
Inline property 会由 Ebox 的 ECSS-backed style domain 编译。需要 selector 驱动规则时使用隔离的 stylesheet:
|
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-style-reset-rules)
|
(ebox-build
|
||||||
(ebox-style-add-rule ".card"
|
'(flex :width (480)
|
||||||
'(:color "#1F2937" :padding '(1 (12)))
|
:flex-flow (row wrap)
|
||||||
:layer 'base)
|
:gap (1 (12))
|
||||||
|
(box :flex (1 1 auto) "Primary")
|
||||||
|
(box :flex-grow 2 "Secondary")))
|
||||||
```
|
```
|
||||||
|
|
||||||
规则使用 ECSS cascade 语义,包括 `:origin`、`:layer` 和 `:scope`。改变规则不会自动发布已经 mounted 的 buffer;规则变更后要对目标 buffer rerender 或 commit。完整 property schema 见[公共 API 参考](ebox-api-reference.zh.md)。
|
### Grid
|
||||||
|
|
||||||
## 4. row、column、flex 与 Grid
|
Grid 放置从 1 开始。轨道可以是固定值、`auto`、分数、`minmax` 或 `repeat`。
|
||||||
|
placement property 同样直接属于子 `box`。
|
||||||
简单的一维组合使用 row 和 column;需要分配剩余空间时使用 flex;需要二维轨道或稳定放置时使用 Grid:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-grid
|
|
||||||
:width '(640)
|
|
||||||
:grid-template-columns '((200) 1fr 1fr)
|
|
||||||
:grid-template-rows '(1 1)
|
|
||||||
:gap '(1 (12))
|
|
||||||
:padding '(1 2)
|
|
||||||
:border '((1) solid "#8A93A6")
|
|
||||||
(ebox-create :content "Header" :grid-column 1 :grid-column-span 3)
|
|
||||||
(ebox-create :content "Navigation" :grid-column 1 :grid-row 2)
|
|
||||||
(ebox-create :content "Main" :grid-column 2 :grid-row 2)
|
|
||||||
(ebox-create :content "Aside" :grid-column 3 :grid-row 2))
|
|
||||||
```
|
|
||||||
|
|
||||||
Grid 轨道支持固定值、分数、`auto`、`minmax` 和重复轨道。显式放置从 1 开始;span 使用正整数;省略的位置由隐式轨道补齐。多行子项会先逐行对齐到自己的 Grid 矩形,再绘制外层边框;即使子项各行的自然宽度不同,带边框的 Grid 右边缘也会保持连续。
|
|
||||||
|
|
||||||
## 5. 渲染文本或 buffer
|
|
||||||
|
|
||||||
`ebox-render` 相对于 buffer 是纯函数,通过临时 TP surface 返回带属性文本;`ebox-render-to-buffer` 会复制声明式 source,并挂载 retained TP surface 完成首次发布:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(let ((node (ebox-column
|
|
||||||
(ebox-create :id "status" :content "Ready" :width '(240))
|
|
||||||
(ebox-create :content "Rendered by Ebox"))))
|
|
||||||
(ebox-render node)
|
|
||||||
(ebox-render-to-buffer "*Ebox Demo*" node))
|
|
||||||
```
|
|
||||||
|
|
||||||
返回文本带有 Ebox 所需的 display、face、region 和 identity 属性,不要手动修改这些属性。source 节点仍由调用者拥有,可以挂载到多个 buffer;每个 buffer 都获得独立的 runtime identity 与状态。
|
|
||||||
|
|
||||||
## 6. 更新已有 buffer
|
|
||||||
|
|
||||||
构造新的根树并提交到已有 buffer:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-commit
|
|
||||||
"*Ebox Demo*"
|
|
||||||
(ebox-column
|
|
||||||
(ebox-create :content "Updated" :key 'title :width '(240))))
|
|
||||||
```
|
|
||||||
|
|
||||||
Ebox 会比较稳定 key 和 region identity,准备 dirty/owner 语义计划,再由 TP 原子发布新的 retained surface 与 Ebox runtime 状态。报告同时保留 Ebox 计划与 TP 执行摘要:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-buffer-update-report "*Ebox Demo*")
|
|
||||||
```
|
|
||||||
|
|
||||||
无法证明安全时会提升到 owner 或 root rerender;render、发布、runtime 状态交换或发布回调失败时,之前的 buffer、TP surface、Ebox runtime 状态和最近一次成功报告都必须保持不变。
|
|
||||||
|
|
||||||
## 7. Selector 与 handle
|
|
||||||
|
|
||||||
Selector 查询已渲染的树并返回公共匹配记录,但不拥有应用状态。给可编辑 box 设置逻辑 `:id`,在某个 live buffer 中解析它,再把不透明、surface-scoped 的 handle 交给 `ebox-region-update`:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(let ((handle (ebox-region-resolve "*Ebox Demo*" "status")))
|
|
||||||
(ebox-region-update handle :content "Ready" :color "#166534"))
|
|
||||||
```
|
|
||||||
|
|
||||||
两个 buffer 中相同的逻辑 id 会解析为两个不同 handle,因此更新一个 surface 不会误改另一个。retained object 被删除或 buffer 被 kill 后,原 handle 会变 stale。`ebox-region-update` 只接受 live handle;数字 region id 只是内部渲染元数据,不是更新 API。
|
|
||||||
|
|
||||||
`ebox-selector-parse` 会把 CSS-like 字符串直接编译成 ECSS 的结构化 selector AST。查询支持 selector list、type、`#id`、`.class`、attribute presence 与 `=`、`~=`、`|=`、`^=`、`$=`、`*=` 操作符、state pseudo、`:is()`、`:where()`、`:not()`、`:has()`、空格 descendant、子节点 `>`、相邻兄弟 `+` 和一般兄弟 `~`。Ebox 只负责把逻辑子节点关系与索引候选映射成 ECSS subject,最终匹配统一由 `ecss-selector-match-p` 决定,因此 query 与 cascade 不会产生两套语义。属性 selector 只看内建的 `:id`/`:key`,以及通过 `:selector-attributes` 显式传入的元数据,例如 `:selector-attributes '((role . button))`;可见 content、布局属性、Ebox runtime 容器与内部 `:ebox-*` 槽位绝不会被隐式转换成 selector 属性。
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-selector-query-buffer "*Ebox Demo*" ".toolbar > box.action")
|
|
||||||
(ebox-selector-query-buffer "*Ebox Demo*" "#first + .later")
|
|
||||||
(ebox-selector-query-buffer "*Ebox Demo*" "#first ~ [role=button]")
|
|
||||||
(ebox-selector-update-buffer "*Ebox Demo*" ".action" :color "#2563EB")
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-selector-update-buffer` 返回 `:matched`、`:updated`、结构化 `:skipped` 和 `:reports`。兼容别名 `ebox-select-all` 与 `ebox-update-selector` 分别指向 query 和 update 函数。
|
|
||||||
|
|
||||||
## 8. Scroll 与 viewport context
|
|
||||||
|
|
||||||
给 box 设置 `:overflow 'scroll`(默认值)和有限的 `:height` 创建 scroll window:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-create :id "log" :width '(420) :height 8
|
|
||||||
:overflow 'scroll
|
|
||||||
:content (mapconcat #'identity lines "\n"))
|
|
||||||
```
|
|
||||||
|
|
||||||
`ebox-scroll-down`、`ebox-scroll-up`、`ebox-scroll-page-down` 和
|
|
||||||
`ebox-scroll-page-up`、`ebox-scroll-down` 等键盘命令从 point 所在的最内层 scroll owner 开始,默认按一行意图分配;内层不足的剩余量继续交给语义父盒子,最后才回退到普通 Emacs buffer scrolling。`ebox-wheel-scroll-down` 与 `ebox-wheel-scroll-up` 以 mouse event 位置为 anchor(不使用 point),采用同样的内到外 residual 路由;没有可消费的 Ebox region 时才委托普通 Emacs scrolling。`ebox-buffer-mode` 本地安装 `ebox-scroll-map`;`ebox-render-to-buffer` 会在返回的 buffer 上启用它。`ebox-scroll-state` 为数字 region id 提供只读 scroll facts;使用滚动命令或 `ebox-region-update` 的 `:scroll-offset` 改变位置。
|
|
||||||
|
|
||||||
Viewport 值使用 `(viewport)` 与 `(viewport-height)`。mounted buffer 要用显式 context 重排:
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(ebox-rerender-buffer-with-context (get-buffer "*Ebox Demo*") 800 30)
|
|
||||||
```
|
|
||||||
|
|
||||||
该调用保留 node 与 region identity。交互式预览中,内容完整且无 chrome 的根 document owner 会在 idle 时一次 materialize,随后采用 Emacs 原生逐行 window scrolling;嵌套或仍处于 lazy 状态的 owner 继续使用事务化 retained-window 路径。[公共 API 参考](ebox-api-reference.zh.md)列出了 lazy prefix、idle prefetch、cache 和滚动配置变量。
|
|
||||||
|
|
||||||
## 9. 独立 `.ebox` 文件
|
|
||||||
|
|
||||||
`ebox-build` 读取一个面向数据的 Ebox form:
|
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
(ebox-build
|
(ebox-build
|
||||||
'(grid :width (640)
|
'(grid :width (640)
|
||||||
:grid-template-columns ((200) 1fr 1fr)
|
:grid-template-columns ((200) 1fr 1fr)
|
||||||
|
:grid-template-rows (1 1)
|
||||||
:gap (1 (12))
|
:gap (1 (12))
|
||||||
(box :content "A")
|
(box :grid-column (1 :span 3) "Header")
|
||||||
(box :content "B")
|
(box :grid-column 1 :grid-row 2 "Navigation")
|
||||||
(box :content "C")))
|
(box :grid-column 2 :grid-row 2 "Main")
|
||||||
|
(box :grid-column 3 :grid-row 2 "Aside")))
|
||||||
```
|
```
|
||||||
|
|
||||||
在 `.ebox` fixture 中,结构表单保持不加 quote。属性位置的 list 和 symbol 常量要加 quote,例如 `:gap '(1 (12))`、`:justify-content 'center`;可执行的 Elisp 属性表达式则保持不加 quote。`ebox-playground` 会在把 form 交给 `ebox-build` 之前求值这些属性表达式,这与 `etaf-view` 的属性值约定一致。
|
## 4. 使用几何与绘制 property
|
||||||
|
|
||||||
可执行的 `.ebox` 参考文件由同级 [`ebox-playground`](../../ebox-playground/README.md) 包维护。它的 `examples/` 目录包含迁移后的 Basic、Comprehensive、Flex、Responsive 以及完整 Grid 参考文件;这些文件只演示低层布局,不依赖 ETAF。
|
普通横向数字表示字符列,`(240)` 这样的单元素 list 表示像素宽度;纵向数字表示
|
||||||
|
行数。Text 只接受字体、前景/背景和文本装饰 property;padding、margin、border、
|
||||||
|
尺寸、`:outer`、overflow、可见性和换行策略只属于 Box。Box 上的字体与颜色可以
|
||||||
|
作为其 Text 子树的继承来源,但不会让 Text 获得 Box 几何。
|
||||||
|
|
||||||
## 10. 可选 native reflow
|
```elisp
|
||||||
|
(ebox-build
|
||||||
|
'(box :width (420)
|
||||||
|
:padding (1 2)
|
||||||
|
:margin (0 1)
|
||||||
|
:border ((1) solid "#8A93A6")
|
||||||
|
:color "#263244"
|
||||||
|
:background-color "#FFFFFF"
|
||||||
|
"A readable panel"))
|
||||||
|
```
|
||||||
|
|
||||||
Rust 模块用于加速符合条件的 reflow,但不是正确性的前提。正常加载 Ebox;需要本地模块时运行 `make native-build`,若模块不在默认位置则配置 `ebox-native-reflow-module-path`。Ebox 保留完全等价的 Elisp 路径,并且加载时不会自动构建 native。`ebox-native-status` 显示 toolchain、module、ABI 与加载诊断;`ebox-native-build` 异步构建,并接受 prefix argument 以清理私有 Cargo cache 后重建。
|
用 `:outer inline` 或 `:outer block` 表达 Box 如何参与父布局。子布局算法仍由 form
|
||||||
|
名称决定。
|
||||||
|
|
||||||
## 11. 公共边界
|
## 5. 渲染与发布
|
||||||
|
|
||||||
使用[公共 API 参考](ebox-api-reference.zh.md)和 `ebox-public-api` 门面清单中列出的函数与 property。以 `ebox--` 开头的名称是私有实现细节,可能随时变化。ETAF 是同级包,负责 Component、View 树、状态、behavior、Context、data 和应用生命周期;Ebox 应保持对几何与发布的专注。
|
`ebox-render` 返回带属性文本,不修改 live buffer;`ebox-render-to-buffer` 挂载
|
||||||
|
retained TP surface 并返回 buffer。
|
||||||
|
|
||||||
## 12. 验证
|
```elisp
|
||||||
|
(ebox-render ebox-guide-root)
|
||||||
|
(ebox-render-to-buffer "*Ebox Guide*" ebox-guide-root)
|
||||||
|
```
|
||||||
|
|
||||||
|
重新构建 root 后,用 `ebox-commit` 原子更新:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-commit
|
||||||
|
"*Ebox Guide*"
|
||||||
|
(ebox-build
|
||||||
|
'(column :padding (1 2)
|
||||||
|
(text "Updated notes")
|
||||||
|
(box :key body "The new root is caller-owned."))))
|
||||||
|
```
|
||||||
|
|
||||||
|
验证、渲染或发布失败时,旧 buffer 与 runtime 状态保持不变。
|
||||||
|
`ebox-buffer-update-report` 返回最近一次成功更新报告的防御性副本。
|
||||||
|
|
||||||
|
## 6. 查询和更新 mounted surface
|
||||||
|
|
||||||
|
`:id`、`:class` 与 `:key` 是 author metadata。Selector 使用 ECSS 语义。直接更新
|
||||||
|
前,把 `:id` 解析为不透明、surface-scoped 的 handle:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(let ((handle (ebox-region-resolve "*Ebox Guide*" "status")))
|
||||||
|
(ebox-region-update handle :color "#166534"))
|
||||||
|
```
|
||||||
|
|
||||||
|
`ebox-selector-query-buffer` 按文档顺序返回 mounted buffer 中的匹配;
|
||||||
|
`ebox-selector-update-buffer` 对所有可编辑匹配应用一次样式更新。数字 region id
|
||||||
|
只是诊断用渲染 metadata,不是稳定更新 handle。
|
||||||
|
|
||||||
|
## 7. Resize 与滚动
|
||||||
|
|
||||||
|
有限高度加 `:overflow scroll` 会创建 scroll window。Ebox buffer mode 安装键盘和
|
||||||
|
滚轮命令:先交给最内层 scroll owner,无法继续消费时才回退到普通 Emacs 滚动。
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-build
|
||||||
|
'(box :id log :width (420) :height 8 :overflow scroll
|
||||||
|
"line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9"))
|
||||||
|
```
|
||||||
|
|
||||||
|
可见 mounted buffer 会跟随其 display window。集成层也可以显式应用 viewport:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-rerender-buffer-with-context
|
||||||
|
(get-buffer "*Ebox Guide*") 800 30)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 独立 `.ebox` 文件
|
||||||
|
|
||||||
|
`.ebox` 文件包含一个不加 quote 的结构 form:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(column :padding '(1 2)
|
||||||
|
(text :color "#263244" "Title")
|
||||||
|
(box :width '(240) "Body"))
|
||||||
|
```
|
||||||
|
|
||||||
|
同级 `ebox-playground` 会先求值 property 表达式,再调用 `ebox-build`。因此 `.ebox`
|
||||||
|
文件中的 list 与 symbol 常量要加 quote,如上所示。直接调用 `ebox-build` 时,则像
|
||||||
|
前面的例子一样传入 inert data。
|
||||||
|
|
||||||
|
## 9. Typed 集成 API
|
||||||
|
|
||||||
|
已经完成 author normalization 的框架可以绕过 list DSL:用 `ebox-text-create`
|
||||||
|
构造 TextNode,用对应 layout constructor 构造 typed LayoutConfig,再用
|
||||||
|
`ebox-box-create` 构造 BoxNode。
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-box-create
|
||||||
|
:layout (ebox-row-layout-create :item-gap 1 :cross-align 'center)
|
||||||
|
:children (list (ebox-text-create :value "Left")
|
||||||
|
(ebox-text-create :value "Right")))
|
||||||
|
```
|
||||||
|
|
||||||
|
这里的 `:layout` 和 `:children` 是 evaluated typed constructor 的字段,不是 author
|
||||||
|
property,也没有扩展七入口语法。
|
||||||
|
|
||||||
|
## 10. 可选 native reflow 与验证
|
||||||
|
|
||||||
|
Rust 模块是可选加速器,Ebox 加载时不会构建它:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-native-status)
|
||||||
|
(ebox-native-build)
|
||||||
|
```
|
||||||
|
|
||||||
在仓库根目录运行:
|
在仓库根目录运行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
|
||||||
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
|
||||||
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
|
||||||
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
|
make dsl-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
|
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
```
|
```
|
||||||
|
|
||||||
局部修改可先运行 `make grid-tests`、`make dsl-tests`、`make selector-tests`、`make surface-tests` 或 `make visual-check-tests`;修改 native 模块后运行 `make native-rust-tests`。
|
|
||||||
|
|||||||
40
ebox-flex.el
40
ebox-flex.el
@ -53,6 +53,7 @@
|
|||||||
(declare-function ebox--maplines "ebox" (function string))
|
(declare-function ebox--maplines "ebox" (function string))
|
||||||
(declare-function ebox--line-has-non-content-properties-p
|
(declare-function ebox--line-has-non-content-properties-p
|
||||||
"ebox" (line))
|
"ebox" (line))
|
||||||
|
(declare-function ebox--line-content-metadata-uniform-p "ebox" (line))
|
||||||
(declare-function ebox--lines-concat-horizontal "ebox" (&rest strings))
|
(declare-function ebox--lines-concat-horizontal "ebox" (&rest strings))
|
||||||
(declare-function ebox--lines-stack-vertical "ebox" (&rest strings))
|
(declare-function ebox--lines-stack-vertical "ebox" (&rest strings))
|
||||||
(declare-function ebox--pixel-border
|
(declare-function ebox--pixel-border
|
||||||
@ -500,8 +501,18 @@ programmatic port and are therefore rejected here."
|
|||||||
"Return non-nil when SOURCE is a layout subtree rather than a single box."
|
"Return non-nil when SOURCE is a layout subtree rather than a single box."
|
||||||
(and (listp source)
|
(and (listp source)
|
||||||
(not (stringp source))
|
(not (stringp source))
|
||||||
(plist-get source :ebox-type)
|
(when-let* ((kind (ebox-tree-layout-kind source)))
|
||||||
(not (eq (plist-get source :ebox-type) 'box))))
|
(not (eq kind 'normal)))))
|
||||||
|
|
||||||
|
(defun ebox--flex-natural-measurement-source-p (source)
|
||||||
|
"Return non-nil when SOURCE has a viewport-independent outer measure pass."
|
||||||
|
(memq (ebox-tree-layout-kind source) '(normal row column)))
|
||||||
|
|
||||||
|
(defun ebox--flex-natural-render-reusable-p (source)
|
||||||
|
"Return non-nil when SOURCE's natural render is independent of item width."
|
||||||
|
(and (ebox--flex-natural-measurement-source-p source)
|
||||||
|
(not (cl-some #'ebox--viewport-dependent-subtree-p
|
||||||
|
(ebox-tree-layout-children source)))))
|
||||||
|
|
||||||
(defun ebox--flex-box-source-p (source)
|
(defun ebox--flex-box-source-p (source)
|
||||||
"Return non-nil when SOURCE owns Box geometry for Flex sizing."
|
"Return non-nil when SOURCE owns Box geometry for Flex sizing."
|
||||||
@ -571,11 +582,11 @@ this avoids signature and retained-cost work for one-off responsive sizes."
|
|||||||
|
|
||||||
(defun ebox--flex-render-source-for-measurement (source viewport)
|
(defun ebox--flex-render-source-for-measurement (source viewport)
|
||||||
"Render SOURCE for flex base-size measurement under VIEWPORT.
|
"Render SOURCE for flex base-size measurement under VIEWPORT.
|
||||||
Direct box flex items are measured intrinsically. Composite items receive
|
Direct Normal, Row, and Column boxes are measured at their natural width.
|
||||||
VIEWPORT for descendants so auto and max-width layouts can resolve against
|
Nested Flex/Grid formatting contexts receive VIEWPORT so responsive descendants
|
||||||
their containing block, while stack/column containers do not stretch their own
|
can resolve against their containing block without conflating every non-Normal
|
||||||
outer measurement width to VIEWPORT."
|
layout with viewport dependence."
|
||||||
(if (ebox--flex-composite-source-p source)
|
(if (memq (ebox-tree-layout-kind source) '(flex grid))
|
||||||
(ebox--flex-bind-source-viewport
|
(ebox--flex-bind-source-viewport
|
||||||
source viewport
|
source viewport
|
||||||
(lambda ()
|
(lambda ()
|
||||||
@ -590,7 +601,8 @@ outer measurement width to VIEWPORT."
|
|||||||
(defun ebox--flex-measured-render-context-compatible-p
|
(defun ebox--flex-measured-render-context-compatible-p
|
||||||
(source measured-viewport final-viewport)
|
(source measured-viewport final-viewport)
|
||||||
"Return non-nil when SOURCE's measured render fits FINAL-VIEWPORT."
|
"Return non-nil when SOURCE's measured render fits FINAL-VIEWPORT."
|
||||||
(or (equal measured-viewport final-viewport)
|
(or (ebox--flex-natural-render-reusable-p source)
|
||||||
|
(equal measured-viewport final-viewport)
|
||||||
(not (ebox--viewport-dependent-subtree-p source))))
|
(not (ebox--viewport-dependent-subtree-p source))))
|
||||||
|
|
||||||
(defun ebox--flex-box-min-main (box rendered axis)
|
(defun ebox--flex-box-min-main (box rendered axis)
|
||||||
@ -766,10 +778,7 @@ grapheme."
|
|||||||
|
|
||||||
(defun ebox--flex-measure-cacheable-p (source props)
|
(defun ebox--flex-measure-cacheable-p (source props)
|
||||||
"Return non-nil when SOURCE and PROPS have stable flex measurements."
|
"Return non-nil when SOURCE and PROPS have stable flex measurements."
|
||||||
(and (or (and (eq (and (listp source)
|
(and (or (ebox--flex-natural-measurement-source-p source)
|
||||||
(plist-get source :ebox-type))
|
|
||||||
'box)
|
|
||||||
(not (ebox--flex-composite-source-p source)))
|
|
||||||
(ebox--render-cacheable-node-p source))
|
(ebox--render-cacheable-node-p source))
|
||||||
(not (ebox--viewport-dependent-size-value-p
|
(not (ebox--viewport-dependent-size-value-p
|
||||||
(plist-get props :flex-basis)))))
|
(plist-get props :flex-basis)))))
|
||||||
@ -779,13 +788,11 @@ grapheme."
|
|||||||
The result is nil when SOURCE cannot use the persistent measurement cache;
|
The result is nil when SOURCE cannot use the persistent measurement cache;
|
||||||
otherwise it carries the key, signature, and any cached measurement so a
|
otherwise it carries the key, signature, and any cached measurement so a
|
||||||
caller can pass the same miss context to the renderer."
|
caller can pass the same miss context to the renderer."
|
||||||
(let ((direct-box-p
|
(let ((natural-source-p (ebox--flex-natural-measurement-source-p source)))
|
||||||
(and (eq (and (listp source) (plist-get source :ebox-type)) 'box)
|
|
||||||
(not (ebox--flex-composite-source-p source)))))
|
|
||||||
;; `ebox--flex-render-source-for-measurement' renders a direct Box with no
|
;; `ebox--flex-render-source-for-measurement' renders a direct Box with no
|
||||||
;; ambient inline viewport. Compute the cache proof in that exact same
|
;; ambient inline viewport. Compute the cache proof in that exact same
|
||||||
;; context so a root resize cannot invalidate an unchanged natural size.
|
;; context so a root resize cannot invalidate an unchanged natural size.
|
||||||
(let ((ebox-viewport-width (if direct-box-p nil ebox-viewport-width)))
|
(let ((ebox-viewport-width (if natural-source-p nil ebox-viewport-width)))
|
||||||
(when (ebox--flex-measure-cacheable-p source props)
|
(when (ebox--flex-measure-cacheable-p source props)
|
||||||
(let* ((cache-key
|
(let* ((cache-key
|
||||||
(list (ebox--ensure-node-id source) 'flex-measure axis props))
|
(list (ebox--ensure-node-id source) 'flex-measure axis props))
|
||||||
@ -1940,6 +1947,7 @@ the chunk begins at the wrapper top and should include top chrome."
|
|||||||
(ebox--maplines
|
(ebox--maplines
|
||||||
(lambda (line)
|
(lambda (line)
|
||||||
(if (and (string-empty-p (string-trim line))
|
(if (and (string-empty-p (string-trim line))
|
||||||
|
(ebox--line-content-metadata-uniform-p line)
|
||||||
(not (ebox--line-has-non-content-properties-p line)))
|
(not (ebox--line-has-non-content-properties-p line)))
|
||||||
(let ((content-id
|
(let ((content-id
|
||||||
(get-text-property 0 'ebox-content line))
|
(get-text-property 0 'ebox-content line))
|
||||||
|
|||||||
@ -331,7 +331,7 @@ unless root reflow and idle prefetch also avoid crossing several such rows.")
|
|||||||
(node-table (plist-get state :node-table)))
|
(node-table (plist-get state :node-table)))
|
||||||
(or (plist-get state :selector-index-stale-p)
|
(or (plist-get state :selector-index-stale-p)
|
||||||
(and (hash-table-p type-counts)
|
(and (hash-table-p type-counts)
|
||||||
(> (or (gethash 'item type-counts) 0) 0))
|
(> (or (gethash 'flex type-counts) 0) 0))
|
||||||
(and (hash-table-p node-table)
|
(and (hash-table-p node-table)
|
||||||
(>= (hash-table-count node-table)
|
(>= (hash-table-count node-table)
|
||||||
ebox--latency-sensitive-scroll-node-limit)))))
|
ebox--latency-sensitive-scroll-node-limit)))))
|
||||||
@ -1136,12 +1136,15 @@ same traversal in complete-render overwrite order."
|
|||||||
(list :parent-node-id node-id
|
(list :parent-node-id node-id
|
||||||
:segment-index segment-index)
|
:segment-index segment-index)
|
||||||
range-ref-table))))
|
range-ref-table))))
|
||||||
(dolist (child (ebox--node-children node))
|
(ebox-tree-for-each-direct-child
|
||||||
|
node
|
||||||
|
(lambda (child)
|
||||||
(visit
|
(visit
|
||||||
child node-id
|
child node-id
|
||||||
(and native-layout-p
|
(and native-layout-p
|
||||||
(not (and (eq type 'flex)
|
(not (and (eq type 'flex)
|
||||||
(eq child (plist-get node :box)))))))
|
(eq child
|
||||||
|
(plist-get node :box))))))))
|
||||||
(index-region-box node)
|
(index-region-box node)
|
||||||
;; This is the same child-before-parent order used by
|
;; This is the same child-before-parent order used by
|
||||||
;; the recursive Layout IR compiler. Flex wrapper
|
;; the recursive Layout IR compiler. Flex wrapper
|
||||||
@ -5792,11 +5795,14 @@ complete preorder first-owner and postorder last-box semantics."
|
|||||||
((visit (node native-layout-p)
|
((visit (node native-layout-p)
|
||||||
(when (and (listp node) (not (stringp node)))
|
(when (and (listp node) (not (stringp node)))
|
||||||
(let ((type (plist-get node :ebox-type)))
|
(let ((type (plist-get node :ebox-type)))
|
||||||
(dolist (child (ebox--node-children node))
|
(ebox-tree-for-each-direct-child
|
||||||
|
node
|
||||||
|
(lambda (child)
|
||||||
(visit child
|
(visit child
|
||||||
(and native-layout-p
|
(and native-layout-p
|
||||||
(not (and (eq type 'flex)
|
(not (and (eq type 'flex)
|
||||||
(eq child (plist-get node :box)))))))
|
(eq child
|
||||||
|
(plist-get node :box))))))))
|
||||||
(when native-layout-p
|
(when native-layout-p
|
||||||
(push node postorder))))))
|
(push node postorder))))))
|
||||||
(visit root t))
|
(visit root t))
|
||||||
@ -5816,19 +5822,38 @@ complete preorder first-owner and postorder last-box semantics."
|
|||||||
(append old-postorder nil)))
|
(append old-postorder nil)))
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
(defun ebox-incremental--candidate-range-owned-node-ids (range-delta-table)
|
(defun ebox-incremental--candidate-range-created-node-ids (range-delta-table)
|
||||||
"Return candidate node ids absorbed by RANGE-DELTA-TABLE parents."
|
"Return new node ids covered by structural RANGE-DELTA-TABLE entries."
|
||||||
(let ((owned (make-hash-table :test 'equal)))
|
(let ((old-ids (make-hash-table :test 'equal))
|
||||||
|
(owned (make-hash-table :test 'equal)))
|
||||||
(when (hash-table-p range-delta-table)
|
(when (hash-table-p range-delta-table)
|
||||||
(cl-labels ((visit
|
(cl-labels ((record
|
||||||
|
(node table)
|
||||||
|
(when (and (listp node) (not (stringp node)))
|
||||||
|
(puthash (plist-get node :node-id) t table)
|
||||||
|
(ebox-tree-for-each-direct-child
|
||||||
|
node (lambda (child) (record child table)))))
|
||||||
|
(record-new
|
||||||
(node)
|
(node)
|
||||||
(when (and (listp node) (not (stringp node)))
|
(when (and (listp node) (not (stringp node)))
|
||||||
(puthash (plist-get node :node-id) t owned)
|
(unless (gethash (plist-get node :node-id) old-ids)
|
||||||
(ebox-tree-for-each-direct-child node #'visit))))
|
(puthash (plist-get node :node-id) t owned))
|
||||||
|
(ebox-tree-for-each-direct-child node #'record-new))))
|
||||||
|
;; Compare identities across the complete Range transaction. A node
|
||||||
|
;; moved between two Range parents is retained, not removed plus
|
||||||
|
;; inserted merely because the two structural deltas have different
|
||||||
|
;; owners.
|
||||||
(maphash
|
(maphash
|
||||||
(lambda (_parent-id delta)
|
(lambda (_parent-id delta)
|
||||||
|
(when (plist-get delta :children-changed)
|
||||||
|
(dolist (payload (plist-get delta :old-payloads))
|
||||||
|
(dolist (node payload) (record node old-ids)))))
|
||||||
|
range-delta-table)
|
||||||
|
(maphash
|
||||||
|
(lambda (_parent-id delta)
|
||||||
|
(when (plist-get delta :children-changed)
|
||||||
(dolist (payload (plist-get delta :new-payloads))
|
(dolist (payload (plist-get delta :new-payloads))
|
||||||
(dolist (node payload) (visit node))))
|
(dolist (node payload) (record-new node)))))
|
||||||
range-delta-table)))
|
range-delta-table)))
|
||||||
owned))
|
owned))
|
||||||
|
|
||||||
@ -5836,8 +5861,8 @@ complete preorder first-owner and postorder last-box semantics."
|
|||||||
(old-state candidate-root touched &optional range-delta-table)
|
(old-state candidate-root touched &optional range-delta-table)
|
||||||
"Return declarative dirty entries for path-local TOUCHED nodes."
|
"Return declarative dirty entries for path-local TOUCHED nodes."
|
||||||
(let ((old-root (plist-get old-state :root-node))
|
(let ((old-root (plist-get old-state :root-node))
|
||||||
(range-owned-node-ids
|
(range-created-node-ids
|
||||||
(ebox-incremental--candidate-range-owned-node-ids range-delta-table))
|
(ebox-incremental--candidate-range-created-node-ids range-delta-table))
|
||||||
dirty)
|
dirty)
|
||||||
(if (not (equal (plist-get old-root :node-id)
|
(if (not (equal (plist-get old-root :node-id)
|
||||||
(plist-get candidate-root :node-id)))
|
(plist-get candidate-root :node-id)))
|
||||||
@ -5853,7 +5878,7 @@ complete preorder first-owner and postorder last-box semantics."
|
|||||||
entry))
|
entry))
|
||||||
(when (and old-node
|
(when (and old-node
|
||||||
(not (gethash (plist-get new-node :node-id)
|
(not (gethash (plist-get new-node :node-id)
|
||||||
range-owned-node-ids)))
|
range-created-node-ids)))
|
||||||
(let* ((changed-keys
|
(let* ((changed-keys
|
||||||
(ebox-incremental--candidate-local-changed-keys
|
(ebox-incremental--candidate-local-changed-keys
|
||||||
old-node new-node))
|
old-node new-node))
|
||||||
|
|||||||
@ -89,6 +89,15 @@
|
|||||||
:kind (ebox-layout-config-kind config)
|
:kind (ebox-layout-config-kind config)
|
||||||
:props (copy-tree (ebox-layout-config-props config))))
|
:props (copy-tree (ebox-layout-config-props config))))
|
||||||
|
|
||||||
|
(defun ebox-layout-config-with-props (config props)
|
||||||
|
"Return CONFIG's variant with detached canonical PROPS.
|
||||||
|
PROPS must already use the variant's normalized representation."
|
||||||
|
(ebox-layout-config-validate config)
|
||||||
|
(ebox-layout-config-validate
|
||||||
|
(ebox-layout-config--create
|
||||||
|
:kind (ebox-layout-config-kind config)
|
||||||
|
:props (copy-tree props))))
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun ebox-normal-layout-create ()
|
(defun ebox-normal-layout-create ()
|
||||||
"Return the canonical Normal layout config."
|
"Return the canonical Normal layout config."
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
(declare-function ebox-lines-join "ebox" (lines))
|
(declare-function ebox-lines-join "ebox" (lines))
|
||||||
(declare-function ebox--maplines "ebox" (function string))
|
(declare-function ebox--maplines "ebox" (function string))
|
||||||
(declare-function ebox--line-has-non-content-properties-p "ebox" (line))
|
(declare-function ebox--line-has-non-content-properties-p "ebox" (line))
|
||||||
|
(declare-function ebox--line-content-metadata-uniform-p "ebox" (line))
|
||||||
(declare-function ebox--lines-align-vertical
|
(declare-function ebox--lines-align-vertical
|
||||||
"ebox" (string height align))
|
"ebox" (string height align))
|
||||||
(declare-function ebox--lines-concat-horizontal "ebox" (&rest strings))
|
(declare-function ebox--lines-concat-horizontal "ebox" (&rest strings))
|
||||||
@ -83,6 +84,9 @@ containing block merely because the row itself is being rendered in it.")
|
|||||||
(defvar ebox--box-content-render-cache nil
|
(defvar ebox--box-content-render-cache nil
|
||||||
"Dynamic render-pass cache for rendered lazy child content.")
|
"Dynamic render-pass cache for rendered lazy child content.")
|
||||||
|
|
||||||
|
(defvar ebox--box-content-cache-purpose nil
|
||||||
|
"Dynamic discriminator for distinct box content materializations.")
|
||||||
|
|
||||||
(defvar ebox--rendered-uniform-width-table
|
(defvar ebox--rendered-uniform-width-table
|
||||||
(make-hash-table :test 'eq :weakness 'key)
|
(make-hash-table :test 'eq :weakness 'key)
|
||||||
"Weak map from rendered strings to their proven uniform pixel width.")
|
"Weak map from rendered strings to their proven uniform pixel width.")
|
||||||
@ -482,6 +486,7 @@ START-INDEX is the first source line index represented by LINES."
|
|||||||
(ebox--propertize-content-line
|
(ebox--propertize-content-line
|
||||||
line region-id index padding-line-filler)))
|
line region-id index padding-line-filler)))
|
||||||
(when (and (string-blank-p line)
|
(when (and (string-blank-p line)
|
||||||
|
(ebox--line-content-metadata-uniform-p line)
|
||||||
(not (ebox--line-has-non-content-properties-p line)))
|
(not (ebox--line-has-non-content-properties-p line)))
|
||||||
(let ((content-id (get-text-property 0 'ebox-content line))
|
(let ((content-id (get-text-property 0 'ebox-content line))
|
||||||
(content-idx
|
(content-idx
|
||||||
@ -897,14 +902,6 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
|||||||
(t
|
(t
|
||||||
(error "ebox: unsupported height value after expansion: %S" value))))
|
(error "ebox: unsupported height value after expansion: %S" value))))
|
||||||
|
|
||||||
(defun ebox--content-max-pixel (box)
|
|
||||||
"Return BOX's max-content width in content pixels."
|
|
||||||
(let* ((content (ebox--box-content box))
|
|
||||||
(lines (ebox-string-lines content)))
|
|
||||||
(if lines
|
|
||||||
(apply #'max (mapcar #'ebox--string-pixel-width lines))
|
|
||||||
0)))
|
|
||||||
|
|
||||||
(defun ebox--box-content-node (box)
|
(defun ebox--box-content-node (box)
|
||||||
"Return BOX's lazy child layout node, if any."
|
"Return BOX's lazy child layout node, if any."
|
||||||
(plist-get box :ebox-content-node))
|
(plist-get box :ebox-content-node))
|
||||||
@ -920,6 +917,7 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
|||||||
ebox-viewport-height
|
ebox-viewport-height
|
||||||
ebox--intrinsic-layout-measurement
|
ebox--intrinsic-layout-measurement
|
||||||
ebox--inline-auto-width-intrinsic-p
|
ebox--inline-auto-width-intrinsic-p
|
||||||
|
ebox--box-content-cache-purpose
|
||||||
ebox--render-region-id))
|
ebox--render-region-id))
|
||||||
|
|
||||||
(defun ebox--render-box-content-cached (box renderer)
|
(defun ebox--render-box-content-cached (box renderer)
|
||||||
@ -1198,6 +1196,43 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
|||||||
content
|
content
|
||||||
(ebox--propertize-typography content box))))
|
(ebox--propertize-typography content box))))
|
||||||
|
|
||||||
|
(defun ebox--intrinsic-box-content (box)
|
||||||
|
"Return BOX content materialized independently of its used width.
|
||||||
|
|
||||||
|
A typed Normal Box performs soft wrapping while composing its children. Its
|
||||||
|
intrinsic widths must therefore measure a separate unwrapped materialization,
|
||||||
|
not the content previously composed for the final viewport. Cache that
|
||||||
|
materialization on the source BOX for the current render pass, while applying
|
||||||
|
probe-only properties to a shallow copy so the canonical node remains
|
||||||
|
unchanged."
|
||||||
|
(if (and (eq (ebox--box-layout-kind box) 'normal)
|
||||||
|
(or (plist-member box :children)
|
||||||
|
(plist-get box :ebox-child-sequence)))
|
||||||
|
(let ((ebox-viewport-width nil)
|
||||||
|
(ebox--intrinsic-layout-measurement t)
|
||||||
|
(ebox--inline-auto-width-intrinsic-p t)
|
||||||
|
(ebox--box-content-cache-purpose 'intrinsic-width))
|
||||||
|
(ebox--render-box-content-cached
|
||||||
|
box
|
||||||
|
(lambda ()
|
||||||
|
(let ((probe (copy-sequence box)))
|
||||||
|
(plist-put probe :width 'auto)
|
||||||
|
(plist-put probe :min-width nil)
|
||||||
|
(plist-put probe :max-width nil)
|
||||||
|
(plist-put probe :wrap-mode 'none)
|
||||||
|
(plist-put probe :ebox-content-width-exact-p nil)
|
||||||
|
(plist-put probe :ebox-content-layout-complete-p nil)
|
||||||
|
(ebox--box-content probe)))))
|
||||||
|
(ebox--box-content box)))
|
||||||
|
|
||||||
|
(defun ebox--content-max-pixel (box)
|
||||||
|
"Return BOX's max-content width in content pixels."
|
||||||
|
(let* ((content (ebox--intrinsic-box-content box))
|
||||||
|
(lines (ebox-string-lines content)))
|
||||||
|
(if lines
|
||||||
|
(apply #'max (mapcar #'ebox--string-pixel-width lines))
|
||||||
|
0)))
|
||||||
|
|
||||||
(defun ebox--line-min-content-pixel (line)
|
(defun ebox--line-min-content-pixel (line)
|
||||||
"Return LINE's min-content width in content pixels."
|
"Return LINE's min-content width in content pixels."
|
||||||
(let ((position 0)
|
(let ((position 0)
|
||||||
@ -1240,7 +1275,7 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
|
|||||||
(let ((wrap-mode (ebox-get box :wrap-mode)))
|
(let ((wrap-mode (ebox-get box :wrap-mode)))
|
||||||
(if (ebox-style-no-soft-wrap-p wrap-mode)
|
(if (ebox-style-no-soft-wrap-p wrap-mode)
|
||||||
(ebox--content-max-pixel box)
|
(ebox--content-max-pixel box)
|
||||||
(let ((lines (ebox-string-lines (ebox--box-content box)))
|
(let ((lines (ebox-string-lines (ebox--intrinsic-box-content box)))
|
||||||
(max-width 0))
|
(max-width 0))
|
||||||
(dolist (line lines max-width)
|
(dolist (line lines max-width)
|
||||||
(setq max-width
|
(setq max-width
|
||||||
@ -1649,6 +1684,7 @@ Internal implementation of `ebox-render' for box nodes."
|
|||||||
(ebox--maplines
|
(ebox--maplines
|
||||||
(lambda (line)
|
(lambda (line)
|
||||||
(if (and (string-empty-p (string-trim line))
|
(if (and (string-empty-p (string-trim line))
|
||||||
|
(ebox--line-content-metadata-uniform-p line)
|
||||||
(not (ebox--line-has-non-content-properties-p line)))
|
(not (ebox--line-has-non-content-properties-p line)))
|
||||||
(let ((content-id (get-text-property 0 'ebox-content line))
|
(let ((content-id (get-text-property 0 'ebox-content line))
|
||||||
(content-idx
|
(content-idx
|
||||||
@ -2047,8 +2083,10 @@ TARGET-WIDTH is the containing block width used to pad narrower leaves."
|
|||||||
(plist-get window :pixel-width))
|
(plist-get window :pixel-width))
|
||||||
((and window (> target-width 0))
|
((and window (> target-width 0))
|
||||||
target-width)
|
target-width)
|
||||||
((eq (and (listp leaf) (plist-get leaf :ebox-type))
|
((and (eq (and (listp leaf)
|
||||||
|
(plist-get leaf :ebox-type))
|
||||||
'box)
|
'box)
|
||||||
|
(not (eq (plist-get leaf :ebox-kind) 'text)))
|
||||||
(ebox--total-pixel leaf))
|
(ebox--total-pixel leaf))
|
||||||
(t
|
(t
|
||||||
(ebox--string-pixel-width rendered))))
|
(ebox--string-pixel-width rendered))))
|
||||||
@ -2934,19 +2972,9 @@ no included vertical margin."
|
|||||||
for source-line in content-lines
|
for source-line in content-lines
|
||||||
for idx from start-index
|
for idx from start-index
|
||||||
collect
|
collect
|
||||||
(let* ((child-ownership-uniform-p
|
(let* ((blank-replacement-p
|
||||||
(cl-every
|
|
||||||
(lambda (property)
|
|
||||||
(not
|
|
||||||
(text-property-not-all
|
|
||||||
0 (length source-line) property
|
|
||||||
(get-text-property 0 property source-line)
|
|
||||||
source-line)))
|
|
||||||
'(ebox-content ebox-content-idx
|
|
||||||
ebox-content-owner ebox-content-owners)))
|
|
||||||
(blank-replacement-p
|
|
||||||
(and (string-blank-p source-line)
|
(and (string-blank-p source-line)
|
||||||
child-ownership-uniform-p
|
(ebox--line-content-metadata-uniform-p source-line)
|
||||||
(not (ebox--line-has-non-content-properties-p
|
(not (ebox--line-has-non-content-properties-p
|
||||||
source-line))))
|
source-line))))
|
||||||
(line
|
(line
|
||||||
@ -3075,6 +3103,7 @@ only used for incomplete lazy prefixes."
|
|||||||
(lambda (line)
|
(lambda (line)
|
||||||
(if (and
|
(if (and
|
||||||
(string-empty-p (string-trim line))
|
(string-empty-p (string-trim line))
|
||||||
|
(ebox--line-content-metadata-uniform-p line)
|
||||||
(not (ebox--line-has-non-content-properties-p line)))
|
(not (ebox--line-has-non-content-properties-p line)))
|
||||||
(let ((content-id
|
(let ((content-id
|
||||||
(get-text-property 0 'ebox-content line))
|
(get-text-property 0 'ebox-content line))
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
(require 'subr-x)
|
(require 'subr-x)
|
||||||
(require 'ecss)
|
(require 'ecss)
|
||||||
(require 'tp-style)
|
(require 'tp-style)
|
||||||
|
(require 'ebox-layout-config)
|
||||||
|
|
||||||
(defvar ebox--longhand)
|
(defvar ebox--longhand)
|
||||||
(declare-function ebox-selector-parse "ebox-selector" (selector))
|
(declare-function ebox-selector-parse "ebox-selector" (selector))
|
||||||
@ -1511,6 +1512,54 @@ until their private encoding is removed."
|
|||||||
(ebox-grid--normalize-config-props raw)))
|
(ebox-grid--normalize-config-props raw)))
|
||||||
(ebox-style--apply-container-wrapper node style snapshot)))
|
(ebox-style--apply-container-wrapper node style snapshot)))
|
||||||
|
|
||||||
|
(defun ebox-style--layout-config-values (style kind snapshot)
|
||||||
|
"Return specified STYLE values owned by KIND's typed LayoutConfig."
|
||||||
|
(let ((names (ebox-layout-config-property-names kind)))
|
||||||
|
(cl-loop for (property value)
|
||||||
|
on (ebox-style--context-values style kind t snapshot)
|
||||||
|
by #'cddr
|
||||||
|
when (memq property names)
|
||||||
|
append (list property value))))
|
||||||
|
|
||||||
|
(defun ebox-style--merge-layout-config-values (base overrides)
|
||||||
|
"Return detached BASE LayoutConfig values updated by OVERRIDES."
|
||||||
|
(let ((result (copy-tree base)))
|
||||||
|
(cl-loop for (property value) on overrides by #'cddr
|
||||||
|
do (setq result (plist-put result property value)))
|
||||||
|
result))
|
||||||
|
|
||||||
|
(defun ebox-style--normalize-layout-config-overrides (kind overrides)
|
||||||
|
"Return specified KIND OVERRIDES in canonical LayoutConfig form."
|
||||||
|
(when overrides
|
||||||
|
(let* ((config (ebox-layout-config-for-form kind overrides))
|
||||||
|
(normalized (ebox-layout-config-props config))
|
||||||
|
(names (cl-loop for (name _value) on overrides by #'cddr
|
||||||
|
collect name)))
|
||||||
|
(cl-loop for name in names
|
||||||
|
append (list name (plist-get normalized name))))))
|
||||||
|
|
||||||
|
(defun ebox-style--apply-typed-layout-config (node style snapshot)
|
||||||
|
"Project computed STYLE child-layout facts into typed Box NODE."
|
||||||
|
(let* ((current (plist-get node :ebox-layout-config))
|
||||||
|
(kind (and current (ebox-layout-config-kind current)))
|
||||||
|
(current-properties
|
||||||
|
(and current
|
||||||
|
(eq kind (ebox-layout-config-kind current))
|
||||||
|
(ebox-layout-config-props current)))
|
||||||
|
(overrides
|
||||||
|
(and (memq kind '(flex grid))
|
||||||
|
(ebox-style--layout-config-values style kind snapshot)))
|
||||||
|
(properties
|
||||||
|
(ebox-style--merge-layout-config-values
|
||||||
|
current-properties
|
||||||
|
(ebox-style--normalize-layout-config-overrides kind overrides))))
|
||||||
|
(when overrides
|
||||||
|
(plist-put node :ebox-layout-config
|
||||||
|
(ebox-layout-config-with-props current properties)))
|
||||||
|
(plist-put node :display
|
||||||
|
(list (or (car-safe (plist-get node :display)) 'block)
|
||||||
|
(if (eq kind 'normal) 'flow kind)))))
|
||||||
|
|
||||||
(defun ebox-style--delete-node-property (node property)
|
(defun ebox-style--delete-node-property (node property)
|
||||||
"Delete PROPERTY from NODE in place while preserving NODE identity."
|
"Delete PROPERTY from NODE in place while preserving NODE identity."
|
||||||
(when (eq (car node) property)
|
(when (eq (car node) property)
|
||||||
@ -1566,18 +1615,21 @@ until their private encoding is removed."
|
|||||||
(unless (ecss-computed-style-p style)
|
(unless (ecss-computed-style-p style)
|
||||||
(signal 'wrong-type-argument (list 'ecss-computed-style-p style)))
|
(signal 'wrong-type-argument (list 'ecss-computed-style-p style)))
|
||||||
(let* ((type (plist-get node :ebox-type))
|
(let* ((type (plist-get node :ebox-type))
|
||||||
(snapshot
|
(config (plist-get node :ebox-layout-config))
|
||||||
(ebox-style--computed-snapshot style (memq type '(flex grid)))))
|
(kind (and config (ebox-layout-config-kind config)))
|
||||||
|
(snapshot (ebox-style--computed-snapshot style t)))
|
||||||
(plist-put node :ebox-computed-style style)
|
(plist-put node :ebox-computed-style style)
|
||||||
(ebox-style--apply-display-axes node style snapshot)
|
(ebox-style--apply-display-axes node style snapshot)
|
||||||
(pcase type
|
(cond
|
||||||
('box
|
((or (memq (plist-get node :ebox-kind) '(text box))
|
||||||
|
(eq type 'box))
|
||||||
(ebox-style--apply-engine-longhands
|
(ebox-style--apply-engine-longhands
|
||||||
node
|
node
|
||||||
(ebox-style--cached-engine-longhands
|
(ebox-style--cached-engine-longhands
|
||||||
style (eq (plist-get node :ebox-kind) 'text) snapshot))
|
style (eq (plist-get node :ebox-kind) 'text) snapshot))
|
||||||
)
|
(when (eq (plist-get node :ebox-kind) 'box)
|
||||||
((or 'flex 'grid)
|
(ebox-style--apply-typed-layout-config node style snapshot)))
|
||||||
|
((memq (or kind type) '(flex grid))
|
||||||
(ebox-style--apply-container node style snapshot)))
|
(ebox-style--apply-container node style snapshot)))
|
||||||
(ebox-style--apply-item node style snapshot))
|
(ebox-style--apply-item node style snapshot))
|
||||||
node)
|
node)
|
||||||
|
|||||||
56
ebox-tree.el
56
ebox-tree.el
@ -30,16 +30,31 @@
|
|||||||
This is an optimization for snapshot capture, not buffer runtime state.")
|
This is an optimization for snapshot capture, not buffer runtime state.")
|
||||||
(defvar ebox--flex-item-prop-keys)
|
(defvar ebox--flex-item-prop-keys)
|
||||||
|
|
||||||
|
(defun ebox-tree-layout-kind (node)
|
||||||
|
"Return NODE's child-layout kind through the tree boundary.
|
||||||
|
Typed Box nodes own this fact in their LayoutConfig. Private legacy nodes are
|
||||||
|
translated only here so tree consumers do not infer layout from storage tags."
|
||||||
|
(let ((canonical-kind (and (listp node) (plist-get node :ebox-kind))))
|
||||||
|
(pcase canonical-kind
|
||||||
|
('text nil)
|
||||||
|
('box
|
||||||
|
(when-let* ((config (plist-get node :ebox-layout-config)))
|
||||||
|
(ebox-layout-config-kind config)))
|
||||||
|
(_
|
||||||
|
(pcase (and (listp node) (plist-get node :ebox-type))
|
||||||
|
('box 'normal)
|
||||||
|
('concat 'row)
|
||||||
|
('stack 'column)
|
||||||
|
('flex 'flex)
|
||||||
|
('grid 'grid)
|
||||||
|
(_ nil))))))
|
||||||
|
|
||||||
(defun ebox-tree-computed-display (node)
|
(defun ebox-tree-computed-display (node)
|
||||||
"Return NODE's canonical CSS-like display pair."
|
"Return NODE's canonical CSS-like display pair."
|
||||||
(or (and (listp node) (plist-get node :display))
|
(or (and (listp node) (plist-get node :display))
|
||||||
(pcase (and (listp node) (plist-get node :ebox-type))
|
(when-let* ((kind (ebox-tree-layout-kind node)))
|
||||||
('box '(block flow))
|
(list 'block (if (eq kind 'normal) 'flow kind)))
|
||||||
('concat '(block row))
|
ebox--default-display))
|
||||||
('stack '(block column))
|
|
||||||
('flex '(block flex))
|
|
||||||
('grid '(block grid))
|
|
||||||
(_ ebox--default-display))))
|
|
||||||
|
|
||||||
(defun ebox-tree-display-outer (node)
|
(defun ebox-tree-display-outer (node)
|
||||||
"Return NODE's outer display."
|
"Return NODE's outer display."
|
||||||
@ -65,17 +80,10 @@ their private props encoding. Callers receive a detached value."
|
|||||||
|
|
||||||
(defun ebox-tree-node-selector-type (node)
|
(defun ebox-tree-node-selector-type (node)
|
||||||
"Return NODE's CSS-like selector type symbol."
|
"Return NODE's CSS-like selector type symbol."
|
||||||
(or (pcase (and (listp node) (plist-get node :ebox-kind))
|
(if (eq (and (listp node) (plist-get node :ebox-kind)) 'text)
|
||||||
('text 'text)
|
'text
|
||||||
('box 'box)
|
(when-let* ((kind (ebox-tree-layout-kind node)))
|
||||||
(_ nil))
|
(if (eq kind 'normal) 'box kind))))
|
||||||
(pcase (and (listp node) (plist-get node :ebox-type))
|
|
||||||
('box 'box)
|
|
||||||
('concat 'row)
|
|
||||||
('stack 'column)
|
|
||||||
('flex 'flex)
|
|
||||||
('grid 'grid)
|
|
||||||
(_ nil))))
|
|
||||||
|
|
||||||
(defun ebox-tree-metadata-string (value)
|
(defun ebox-tree-metadata-string (value)
|
||||||
"Return VALUE normalized to a selector metadata string."
|
"Return VALUE normalized to a selector metadata string."
|
||||||
@ -691,16 +699,10 @@ Host references are root-global opaque metadata and are compared with `equal'."
|
|||||||
(defun ebox-tree--child-layout-kind (node parent-kind)
|
(defun ebox-tree--child-layout-kind (node parent-kind)
|
||||||
"Return the layout kind governing NODE's direct children."
|
"Return the layout kind governing NODE's direct children."
|
||||||
(ignore parent-kind)
|
(ignore parent-kind)
|
||||||
(pcase (plist-get node :ebox-type)
|
(or (ebox-tree-layout-kind node)
|
||||||
('flex 'flex)
|
(pcase (ebox-tree-display-inner node)
|
||||||
('grid 'grid)
|
|
||||||
(_ (pcase (if (and (eq (plist-get node :ebox-kind) 'box)
|
|
||||||
(plist-get node :ebox-layout-config))
|
|
||||||
(ebox-layout-config-kind
|
|
||||||
(plist-get node :ebox-layout-config))
|
|
||||||
(ebox-tree-display-inner node))
|
|
||||||
('flow 'normal)
|
('flow 'normal)
|
||||||
(kind kind)))))
|
(kind kind))))
|
||||||
|
|
||||||
(defun ebox-tree--validate-node-parent-participation
|
(defun ebox-tree--validate-node-parent-participation
|
||||||
(node parent-kind source)
|
(node parent-kind source)
|
||||||
|
|||||||
16
ebox.el
16
ebox.el
@ -546,6 +546,22 @@ and incremental diffs byte-compatible with the visible rendering."
|
|||||||
(setq pos (or (next-property-change pos line) len))))
|
(setq pos (or (next-property-change pos line) len))))
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
|
(defun ebox--line-content-metadata-uniform-p (line)
|
||||||
|
"Return non-nil when LINE has one uniform content metadata run.
|
||||||
|
Blank-line normalization may replace a line with one full-width filler only
|
||||||
|
under this condition; otherwise replacement would erase child ownership
|
||||||
|
segments that incremental publication still needs."
|
||||||
|
(let ((length (length line)))
|
||||||
|
(or (zerop length)
|
||||||
|
(cl-every
|
||||||
|
(lambda (property)
|
||||||
|
(not (text-property-not-all
|
||||||
|
0 length property
|
||||||
|
(get-text-property 0 property line)
|
||||||
|
line)))
|
||||||
|
'(ebox-content ebox-content-idx
|
||||||
|
ebox-content-owner ebox-content-owners)))))
|
||||||
|
|
||||||
(defun ebox--lines-pad-vertical (string height &optional offset padding-string)
|
(defun ebox--lines-pad-vertical (string height &optional offset padding-string)
|
||||||
"Extend STRING to HEIGHT lines.
|
"Extend STRING to HEIGHT lines.
|
||||||
OFFSET controls content shift: positive (top), negative (bottom).
|
OFFSET controls content shift: positive (top), negative (bottom).
|
||||||
|
|||||||
@ -279,49 +279,52 @@
|
|||||||
(ebox--with-buffer-render-context buffer
|
(ebox--with-buffer-render-context buffer
|
||||||
(ebox-render (ebox--buffer-root-node buffer)))))
|
(ebox-render (ebox--buffer-root-node buffer)))))
|
||||||
|
|
||||||
(defun ebox-performance-evaluator--static-fixture (&optional styled)
|
(defun ebox-performance-evaluator--static-fixture
|
||||||
"Return a deterministic nested fixture, optionally marked STYLED."
|
(&optional styled target-content)
|
||||||
|
"Return a deterministic fixture, optionally STYLED and with TARGET-CONTENT."
|
||||||
(let (rows)
|
(let (rows)
|
||||||
(dotimes (row 5)
|
(dotimes (row 5)
|
||||||
(let (cells)
|
(let (cells)
|
||||||
(dotimes (column 4)
|
(dotimes (column 4)
|
||||||
(push
|
(push
|
||||||
(apply #'ebox-test-box
|
|
||||||
(append
|
(append
|
||||||
(list :key (list 'cell row column)
|
(list 'box
|
||||||
:content (format "row-%d cell-%d\ndetail-%d-%d"
|
:key (list 'cell row column)
|
||||||
row column row column)
|
|
||||||
:width '(52)
|
:width '(52)
|
||||||
:padding '(0 1)
|
:padding '(0 1)
|
||||||
:color "#0F172A"
|
:color "#0F172A"
|
||||||
:bgcolor "#F8FAFC")
|
:bgcolor "#F8FAFC")
|
||||||
(when (and (= row 2) (= column 1))
|
(when (and (= row 2) (= column 1)) '(:id target))
|
||||||
'(:id target))
|
(when styled '(:class card))
|
||||||
(when styled '(:class card))))
|
(list (if (and target-content (= row 2) (= column 1))
|
||||||
|
target-content
|
||||||
|
(format "row-%d cell-%d\ndetail-%d-%d"
|
||||||
|
row column row column))))
|
||||||
cells))
|
cells))
|
||||||
(push (apply #'ebox-test-row (nreverse cells)) rows)))
|
(push (cons 'row (nreverse cells)) rows)))
|
||||||
(apply #'ebox-test-column (nreverse rows))))
|
(ebox-build (cons 'column (nreverse rows)))))
|
||||||
|
|
||||||
(defun ebox-performance-evaluator--resize-fixture ()
|
(defun ebox-performance-evaluator--resize-fixture ()
|
||||||
"Return a viewport-dependent wrapped flex fixture."
|
"Return a viewport-dependent wrapped flex fixture."
|
||||||
(let (items)
|
(let (items)
|
||||||
(dotimes (index 12)
|
(dotimes (index 12)
|
||||||
(push
|
(push
|
||||||
(ebox-test-box
|
(list 'box
|
||||||
:key (list 'resize-item index)
|
:key (list 'resize-item index)
|
||||||
:content (format "resize-%02d" index)
|
|
||||||
:width '(70)
|
:width '(70)
|
||||||
:padding '(0 1)
|
:padding '(0 1)
|
||||||
:color "#172554"
|
:color "#172554"
|
||||||
:bgcolor "#DBEAFE")
|
:bgcolor "#DBEAFE"
|
||||||
|
(format "resize-%02d" index))
|
||||||
items))
|
items))
|
||||||
(apply #'ebox-test-flex
|
(ebox-build
|
||||||
|
(append (list 'flex
|
||||||
:key 'resize-root
|
:key 'resize-root
|
||||||
:width '(viewport)
|
:width '(viewport)
|
||||||
:flex-wrap 'wrap
|
:flex-wrap 'wrap
|
||||||
:column-gap '(6)
|
:column-gap '(6)
|
||||||
:row-gap 1
|
:row-gap 1)
|
||||||
(nreverse items))))
|
(nreverse items)))))
|
||||||
|
|
||||||
(defun ebox-performance-evaluator--pure-static ()
|
(defun ebox-performance-evaluator--pure-static ()
|
||||||
"Measure isolated static materialization against a TP-backed reference."
|
"Measure isolated static materialization against a TP-backed reference."
|
||||||
@ -399,7 +402,7 @@
|
|||||||
:checks checks :tp-report nil)))))
|
:checks checks :tp-report nil)))))
|
||||||
|
|
||||||
(defun ebox-performance-evaluator--mounted-update ()
|
(defun ebox-performance-evaluator--mounted-update ()
|
||||||
"Measure one scoped mounted content update and validate its output."
|
"Measure one scoped mounted content commit and validate its output."
|
||||||
(let ((ebox-style-stylesheet (ecss-stylesheet-create))
|
(let ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||||
(ebox-viewport-width 240)
|
(ebox-viewport-width 240)
|
||||||
(ebox-viewport-height 12)
|
(ebox-viewport-height 12)
|
||||||
@ -417,12 +420,16 @@
|
|||||||
(measurement
|
(measurement
|
||||||
(ebox-performance-evaluator--measure
|
(ebox-performance-evaluator--measure
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(ebox-region-update handle :content "UPDATED\ndetail"))))
|
(ebox-commit
|
||||||
|
buffer
|
||||||
|
(ebox-performance-evaluator--static-fixture
|
||||||
|
nil "UPDATED\ndetail")))))
|
||||||
(report (ebox-buffer-update-report buffer))
|
(report (ebox-buffer-update-report buffer))
|
||||||
(surface (plist-get (ebox--buffer-render-state buffer)
|
(surface (plist-get (ebox--buffer-render-state buffer)
|
||||||
:surface))
|
:surface))
|
||||||
|
(after-handle (ebox-region-resolve buffer "target"))
|
||||||
(after-object
|
(after-object
|
||||||
(ebox-selector--region-handle-object handle))
|
(ebox-selector--region-handle-object after-handle))
|
||||||
(expected (ebox-performance-evaluator--runtime-output buffer))
|
(expected (ebox-performance-evaluator--runtime-output buffer))
|
||||||
(actual (ebox-performance-evaluator--buffer-string buffer))
|
(actual (ebox-performance-evaluator--buffer-string buffer))
|
||||||
(tp-report (tp-surface-report surface))
|
(tp-report (tp-surface-report surface))
|
||||||
|
|||||||
@ -28,58 +28,29 @@
|
|||||||
|
|
||||||
(defun ebox-visual-check-layout ()
|
(defun ebox-visual-check-layout ()
|
||||||
"Return the fixed layout used by visual verification."
|
"Return the fixed layout used by visual verification."
|
||||||
(let* ((title (ebox-test-box
|
(ebox-build
|
||||||
:content "Ebox Visual Check"
|
'(column :id dashboard
|
||||||
:box-sizing 'border-box
|
(box :id title :box-sizing border-box :width 74
|
||||||
:width 74
|
:padding (0 1) :border "#4A90D9"
|
||||||
:padding '(0 1)
|
:color "#EAF6FF" :bgcolor "#203040"
|
||||||
:border t
|
"Ebox Visual Check")
|
||||||
:border-color "#4A90D9"
|
(row :id cards
|
||||||
:color "#EAF6FF"
|
(box :id hidden :box-sizing border-box :width 24 :height 3
|
||||||
:bgcolor "#203040"))
|
:padding (1 1) :border "#27AE60"
|
||||||
(hidden (ebox-test-box
|
:color "#F0FFF4" :bgcolor "#17351F" :overflow hidden
|
||||||
:content "Hidden: visible line\nHIDDEN-CLIPPED-LINE"
|
"Hidden: visible line\nHIDDEN-CLIPPED-LINE")
|
||||||
:box-sizing 'border-box
|
(box :id scroll :box-sizing border-box :width 24 :height 3
|
||||||
:width 24
|
:padding (1 1) :border "#E67E22"
|
||||||
:height 3
|
:color "#FFF4E6" :bgcolor "#3A2410" :overflow scroll
|
||||||
:padding '(1 1)
|
"Scroll: first\nSCROLL-SECOND\nSCROLL-THIRD")
|
||||||
:border t
|
(box :id sized :box-sizing border-box :width 26 :max-height 3
|
||||||
:border-color "#27AE60"
|
:padding (1 1) :border "#9B59B6"
|
||||||
:color "#F0FFF4"
|
:color "#F8EEFF" :bgcolor "#2F1B3A" :overflow hidden
|
||||||
:bgcolor "#17351F"
|
"border-box max-height\nline 2\nSIZED-CLIPPED"))
|
||||||
:overflow 'hidden))
|
(box :id footer :box-sizing border-box :width 74
|
||||||
(scroll (ebox-test-box
|
:padding (0 1) :border "#7F8C8D"
|
||||||
:content "Scroll: first\nSCROLL-SECOND\nSCROLL-THIRD"
|
:color "#F8F9F9" :bgcolor "#2C3E50"
|
||||||
:box-sizing 'border-box
|
"Checks: text properties + display specs + GUI screenshot"))))
|
||||||
:width 24
|
|
||||||
:height 3
|
|
||||||
:padding '(1 1)
|
|
||||||
:border t
|
|
||||||
:border-color "#E67E22"
|
|
||||||
:color "#FFF4E6"
|
|
||||||
:bgcolor "#3A2410"
|
|
||||||
:overflow 'scroll))
|
|
||||||
(sized (ebox-test-box
|
|
||||||
:content "border-box max-height\nline 2\nSIZED-CLIPPED"
|
|
||||||
:box-sizing 'border-box
|
|
||||||
:width 26
|
|
||||||
:max-height 3
|
|
||||||
:padding '(1 1)
|
|
||||||
:border t
|
|
||||||
:border-color "#9B59B6"
|
|
||||||
:color "#F8EEFF"
|
|
||||||
:bgcolor "#2F1B3A"
|
|
||||||
:overflow 'hidden))
|
|
||||||
(footer (ebox-test-box
|
|
||||||
:content "Checks: text properties + display specs + GUI screenshot"
|
|
||||||
:box-sizing 'border-box
|
|
||||||
:width 74
|
|
||||||
:padding '(0 1)
|
|
||||||
:border t
|
|
||||||
:border-color "#7F8C8D"
|
|
||||||
:color "#F8F9F9"
|
|
||||||
:bgcolor "#2C3E50")))
|
|
||||||
(ebox-test-column title (ebox-test-row hidden scroll sized) footer)))
|
|
||||||
|
|
||||||
(defun ebox-visual-check--max-line-pixel-width (string)
|
(defun ebox-visual-check--max-line-pixel-width (string)
|
||||||
"Return maximum pixel width among STRING lines."
|
"Return maximum pixel width among STRING lines."
|
||||||
@ -131,12 +102,20 @@
|
|||||||
"Return visual verification report based on rendered text properties."
|
"Return visual verification report based on rendered text properties."
|
||||||
(let* ((layout (ebox-visual-check-layout))
|
(let* ((layout (ebox-visual-check-layout))
|
||||||
(ids-before (ebox-region-ids layout))
|
(ids-before (ebox-region-ids layout))
|
||||||
(rendered (ebox-render layout))
|
(buffer (generate-new-buffer " *ebox-visual-check-report*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-render-to-buffer buffer layout)
|
||||||
|
(let* ((runtime-root (ebox--buffer-root-node buffer))
|
||||||
|
(ids-after (ebox-region-ids runtime-root))
|
||||||
|
(rendered
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(buffer-substring (point-min) (point-max))))
|
||||||
(plain (substring-no-properties rendered))
|
(plain (substring-no-properties rendered))
|
||||||
(ids-after (ebox-region-ids rendered))
|
|
||||||
(metrics (list :height (ebox-string-height rendered)
|
(metrics (list :height (ebox-string-height rendered)
|
||||||
:pixel-width
|
:pixel-width
|
||||||
(ebox-visual-check--max-line-pixel-width rendered)
|
(ebox-visual-check--max-line-pixel-width
|
||||||
|
rendered)
|
||||||
:region-count (length ids-before))))
|
:region-count (length ids-before))))
|
||||||
(list
|
(list
|
||||||
(list
|
(list
|
||||||
@ -145,13 +124,16 @@
|
|||||||
:checks
|
:checks
|
||||||
(list
|
(list
|
||||||
(ebox-visual-check--check
|
(ebox-visual-check--check
|
||||||
"region ids are stable"
|
"source and mounted identities agree"
|
||||||
(equal ids-before ids-after)
|
(equal ids-before ids-after)
|
||||||
(format "before=%S after=%S" ids-before ids-after))
|
(format "source=%S mounted=%S" ids-before ids-after))
|
||||||
(ebox-visual-check--check
|
(ebox-visual-check--check
|
||||||
"all expected boxes rendered"
|
"expected visible labels rendered"
|
||||||
(= (length ids-before) 5)
|
(cl-every (lambda (label) (string-match-p label plain))
|
||||||
(format "region-count=%d" (length ids-before)))
|
'("Ebox Visual Check" "Hidden: visible line"
|
||||||
|
"Scroll: first" "border-box max-height"
|
||||||
|
"Checks: text properties"))
|
||||||
|
"all non-clipped fixture sections are present")
|
||||||
(ebox-visual-check--check
|
(ebox-visual-check--check
|
||||||
"display specs exist"
|
"display specs exist"
|
||||||
(ebox-visual-check--some-property-p rendered 'display)
|
(ebox-visual-check--some-property-p rendered 'display)
|
||||||
@ -177,6 +159,8 @@
|
|||||||
"border-box max-height clips outer box"
|
"border-box max-height clips outer box"
|
||||||
(not (string-match-p "SIZED-CLIPPED" plain))
|
(not (string-match-p "SIZED-CLIPPED" plain))
|
||||||
"max-height includes vertical padding"))))))
|
"max-height includes vertical padding"))))))
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
(defun ebox-visual-check--format-report (report)
|
(defun ebox-visual-check--format-report (report)
|
||||||
"Format REPORT as human-readable text."
|
"Format REPORT as human-readable text."
|
||||||
|
|||||||
@ -599,7 +599,6 @@
|
|||||||
(unwind-protect
|
(unwind-protect
|
||||||
(progn
|
(progn
|
||||||
(ebox-render-to-buffer buffer (root))
|
(ebox-render-to-buffer buffer (root))
|
||||||
(let ((root-id (ebox--buffer-root-node-id buffer)))
|
|
||||||
(dolist (selected '(2 1 2))
|
(dolist (selected '(2 1 2))
|
||||||
(let ((candidate (ebox-candidate-begin buffer)))
|
(let ((candidate (ebox-candidate-begin buffer)))
|
||||||
(ebox-candidate-replace-range-ref
|
(ebox-candidate-replace-range-ref
|
||||||
@ -617,15 +616,19 @@
|
|||||||
selected report))
|
selected report))
|
||||||
(should-not (memq 'structure
|
(should-not (memq 'structure
|
||||||
(plist-get report :dirty-kinds)))
|
(plist-get report :dirty-kinds)))
|
||||||
(should-not (member root-id
|
(should (memq 'paint
|
||||||
(plist-get report :owner-ids)))
|
(plist-get report :dirty-kinds)))
|
||||||
(should (memq (plist-get report :strategy)
|
(should (memq (plist-get report :strategy)
|
||||||
'(span-patch owner-rerender
|
'(span-patch owner-rerender
|
||||||
mixed-owner-reflow)))
|
mixed-owner-reflow)))
|
||||||
(should (> (plist-get report :tp-scope-count) 0))
|
(should (> (plist-get report :tp-scope-count) 0))
|
||||||
(should-not (plist-get report :tp-full-root))
|
(should-not (plist-get report :tp-full-root))
|
||||||
(should (= (plist-get report :created-objects) 0))
|
(should (= (plist-get report :created-objects) 0))
|
||||||
(should (= (plist-get report :removed-objects) 0))))))))
|
(should (= (plist-get report :removed-objects) 0))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(should (string-match-p
|
||||||
|
(format "detail-%d" selected)
|
||||||
|
(buffer-string)))))))))
|
||||||
(when (buffer-live-p buffer) (kill-buffer buffer))))))
|
(when (buffer-live-p buffer) (kill-buffer buffer))))))
|
||||||
|
|
||||||
(ert-deftest ebox-child-range-does-not-reuse-disappeared-key-positionally ()
|
(ert-deftest ebox-child-range-does-not-reuse-disappeared-key-positionally ()
|
||||||
|
|||||||
@ -941,8 +941,8 @@ remain retained identities."
|
|||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer buffer)))))))
|
(kill-buffer buffer)))))))
|
||||||
|
|
||||||
(ert-deftest ebox-commit-updates-canonical-runtime-type-counts ()
|
(ert-deftest ebox-commit-updates-selector-type-counts ()
|
||||||
"A structural candidate should publish exact Text and Box counts."
|
"A structural candidate should publish exact author selector-type counts."
|
||||||
(let* ((buffer
|
(let* ((buffer
|
||||||
(ebox-render-to-buffer
|
(ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-commit-types*")
|
(generate-new-buffer-name " *ebox-commit-types*")
|
||||||
@ -954,15 +954,20 @@ remain retained identities."
|
|||||||
(progn
|
(progn
|
||||||
(should (= (gethash
|
(should (= (gethash
|
||||||
'box (plist-get before :runtime-type-count-table))
|
'box (plist-get before :runtime-type-count-table))
|
||||||
2))
|
1))
|
||||||
|
(should (= (gethash
|
||||||
|
'flex (plist-get before :runtime-type-count-table))
|
||||||
|
1))
|
||||||
(should (= (gethash
|
(should (= (gethash
|
||||||
'text (plist-get before :runtime-type-count-table))
|
'text (plist-get before :runtime-type-count-table))
|
||||||
1))
|
1))
|
||||||
(ebox-commit
|
(ebox-commit
|
||||||
buffer (ebox-test-box :key 'root :content "B" :width '(80)))
|
buffer (ebox-build '(box :key root :width (80) "B")))
|
||||||
(let ((after (gethash buffer ebox--buffer-render-state-table)))
|
(let ((after (gethash buffer ebox--buffer-render-state-table)))
|
||||||
(should-not
|
(should-not
|
||||||
(gethash 'item (plist-get after :runtime-type-count-table)))
|
(gethash 'item (plist-get after :runtime-type-count-table)))
|
||||||
|
(should-not
|
||||||
|
(gethash 'flex (plist-get after :runtime-type-count-table)))
|
||||||
(should (= (gethash
|
(should (= (gethash
|
||||||
'box (plist-get after :runtime-type-count-table))
|
'box (plist-get after :runtime-type-count-table))
|
||||||
1))
|
1))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,12 @@
|
|||||||
"docs/maintainer/ebox-incremental-update-contract.zh.md")
|
"docs/maintainer/ebox-incremental-update-contract.zh.md")
|
||||||
"Long-lived documentation files checked for stale path references.")
|
"Long-lived documentation files checked for stale path references.")
|
||||||
|
|
||||||
|
(defconst ebox-docs-test--public-user-docs
|
||||||
|
'("README.md" "README.zh-CN.md"
|
||||||
|
"docs/user/ebox-user-guide.en.md" "docs/user/ebox-user-guide.zh.md"
|
||||||
|
"docs/user/ebox-api-reference.en.md" "docs/user/ebox-api-reference.zh.md")
|
||||||
|
"Public documents that must describe only the canonical author grammar.")
|
||||||
|
|
||||||
(defconst ebox-docs-test--active-files
|
(defconst ebox-docs-test--active-files
|
||||||
'("Makefile" ".github/workflows/ci.yml"
|
'("Makefile" ".github/workflows/ci.yml"
|
||||||
"ebox.el" "ebox-cache.el" "ebox-style.el" "ebox-child-range.el" "ebox-tree.el"
|
"ebox.el" "ebox-cache.el" "ebox-style.el" "ebox-child-range.el" "ebox-tree.el"
|
||||||
@ -110,6 +116,36 @@
|
|||||||
(should-not (string-match-p "ebox-grid-item" source))
|
(should-not (string-match-p "ebox-grid-item" source))
|
||||||
(should-not (string-match-p "`grid-item`" source)))))
|
(should-not (string-match-p "`grid-item`" source)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-docs-public-author-surface-excludes-removed-forms ()
|
||||||
|
"Public docs must not revive removed constructors or field-based children."
|
||||||
|
(dolist (file ebox-docs-test--public-user-docs)
|
||||||
|
(let ((source (ebox-docs-test--read file)))
|
||||||
|
(dolist (removed '("ebox-create" "ebox-concat" "ebox-stack"
|
||||||
|
"ebox-spacer" "ebox-flex-item" "ebox-grid-item"
|
||||||
|
"raw-ebox" ":content"))
|
||||||
|
(should-not (string-match-p (regexp-quote removed) source)))
|
||||||
|
(dolist (removed-form '("(item " "(spacer " "(raw-ebox "))
|
||||||
|
(should-not (string-match-p (regexp-quote removed-form) source))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-docs-canonical-author-grammar-executes ()
|
||||||
|
"Every documented author entry must build the advertised typed node."
|
||||||
|
(require 'ebox)
|
||||||
|
(let ((text-forms '("short" (text "explicit"))))
|
||||||
|
(dolist (form text-forms)
|
||||||
|
(should (ebox-text-node-p (ebox-build form)))))
|
||||||
|
(dolist (entry '((box "normal")
|
||||||
|
(row (box "row child"))
|
||||||
|
(column (box "column child"))
|
||||||
|
(flex (box "flex child"))
|
||||||
|
(grid (box "grid child"))))
|
||||||
|
(let* ((node (ebox-build entry))
|
||||||
|
(layout (ebox-box-node-layout node)))
|
||||||
|
(should (ebox-box-node-p node))
|
||||||
|
(should (eq (ebox-layout-config-kind layout)
|
||||||
|
(pcase (car entry)
|
||||||
|
('box 'normal)
|
||||||
|
(kind kind)))))))
|
||||||
|
|
||||||
(provide 'ebox-docs-contract-tests)
|
(provide 'ebox-docs-contract-tests)
|
||||||
|
|
||||||
;;; ebox-docs-contract-tests.el ends here
|
;;; ebox-docs-contract-tests.el ends here
|
||||||
|
|||||||
@ -49,17 +49,28 @@
|
|||||||
(should (eq (ebox-style-dirty-kind :layout) 'structure))
|
(should (eq (ebox-style-dirty-kind :layout) 'structure))
|
||||||
(should (eq (ebox-style-dirty-kind :display) 'structure)))
|
(should (eq (ebox-style-dirty-kind :display) 'structure)))
|
||||||
|
|
||||||
(ert-deftest ebox-style-projects-public-box-axes-to-internal-display ()
|
(ert-deftest ebox-typed-form-projects-outer-and-layout-to-display ()
|
||||||
"Computed outer/layout properties should project to one display pair."
|
"A typed Box form should project its two axes to one display pair."
|
||||||
(let* ((style
|
(let ((node (ebox-build '(flex :outer inline "A"))))
|
||||||
(ebox-style-compute-subject
|
|
||||||
(ecss-subject-create :type "box")
|
|
||||||
(ebox-style-compile-declarations
|
|
||||||
'(:outer inline :layout flex))))
|
|
||||||
(node (ebox-build '(box "A"))))
|
|
||||||
(ebox-style-apply-computed node style)
|
|
||||||
(should (equal (ebox--computed-display node) '(inline flex)))))
|
(should (equal (ebox--computed-display node) '(inline flex)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-style-unrelated-facts-preserve-typed-layout-config ()
|
||||||
|
"Unrelated computed facts must not replace direct typed layout values."
|
||||||
|
(dolist (case '((flex
|
||||||
|
(flex :flex-direction column :column-gap (6) "A"))
|
||||||
|
(grid
|
||||||
|
(grid :grid-template-columns ((40) (60))
|
||||||
|
:column-gap (4) "A"))))
|
||||||
|
(let* ((kind (car case))
|
||||||
|
(node (ebox-build (cadr case)))
|
||||||
|
(before (copy-tree (plist-get node :ebox-layout-config)))
|
||||||
|
(style
|
||||||
|
(ebox-style-compute-subject
|
||||||
|
(ecss-subject-create :type (symbol-name kind))
|
||||||
|
(ebox-style-compile-declarations '(:color "#123456")))))
|
||||||
|
(ebox-style-apply-computed node style)
|
||||||
|
(should (equal (plist-get node :ebox-layout-config) before)))))
|
||||||
|
|
||||||
(ert-deftest ebox-style-strict-cache-does-not-hide-unknown-properties ()
|
(ert-deftest ebox-style-strict-cache-does-not-hide-unknown-properties ()
|
||||||
"Strict declaration validation should remain exact after an empty cache hit."
|
"Strict declaration validation should remain exact after an empty cache hit."
|
||||||
(ebox-style-compile-declarations nil t)
|
(ebox-style-compile-declarations nil t)
|
||||||
@ -828,6 +839,12 @@
|
|||||||
:bgcolor "red")
|
:bgcolor "red")
|
||||||
:type 'error))
|
:type 'error))
|
||||||
|
|
||||||
|
(ert-deftest ebox-build-projects-source-style-without-polluting-canonical-geometry ()
|
||||||
|
"Track the G4 boundary: author paint must leave canonical Box geometry."
|
||||||
|
:expected-result :failed
|
||||||
|
(let ((node (ebox-build '(box :background-color "red" "Paint"))))
|
||||||
|
(should-not (ebox-get node :bgcolor))))
|
||||||
|
|
||||||
(ert-deftest ebox-canonical-box-requires-unique-reserved-fields ()
|
(ert-deftest ebox-canonical-box-requires-unique-reserved-fields ()
|
||||||
"Typed Box reserved fields should never be missing or duplicated."
|
"Typed Box reserved fields should never be missing or duplicated."
|
||||||
(let ((normal (ebox-normal-layout-create)))
|
(let ((normal (ebox-normal-layout-create)))
|
||||||
@ -867,42 +884,6 @@
|
|||||||
(let ((inline-row (ebox-build '(row :outer inline "A" "B"))))
|
(let ((inline-row (ebox-build '(row :outer inline "A" "B"))))
|
||||||
(should (equal (ebox--computed-display inline-row) '(inline row)))))
|
(should (equal (ebox--computed-display inline-row) '(inline row)))))
|
||||||
|
|
||||||
(ert-deftest ebox-build-projects-source-style-without-polluting-canonical-geometry ()
|
|
||||||
"Source metadata and author paint should use their orthogonal projections."
|
|
||||||
(let* ((form
|
|
||||||
'(box :key root :id "root" :class "card"
|
|
||||||
:background-color "red" :width '(80)
|
|
||||||
(text :color "blue" "A")))
|
|
||||||
(original (symbol-function 'ebox-style-compile-declarations))
|
|
||||||
(compile-calls 0)
|
|
||||||
(node
|
|
||||||
(cl-letf (((symbol-function 'ebox-style-compile-declarations)
|
|
||||||
(lambda (&rest arguments)
|
|
||||||
(cl-incf compile-calls)
|
|
||||||
(apply original arguments))))
|
|
||||||
(ebox-build form)))
|
|
||||||
(child (car (ebox-box-node-children node)))
|
|
||||||
(rendered (ebox-render node))
|
|
||||||
(face (get-text-property 0 'face rendered)))
|
|
||||||
(should (eq (plist-get node :key) 'root))
|
|
||||||
(should (equal (plist-get node :id) "root"))
|
|
||||||
(should (equal (plist-get node :class) "card"))
|
|
||||||
(should (eq (plist-get node :host-ref)
|
|
||||||
(ebox-node-source-handle node)))
|
|
||||||
(should (eq (plist-get child :host-ref)
|
|
||||||
(ebox-node-source-handle child)))
|
|
||||||
(should (= compile-calls 2))
|
|
||||||
(should (equal (ebox-get node :bgcolor) nil))
|
|
||||||
(should
|
|
||||||
(cl-some (lambda (entry)
|
|
||||||
(and (listp entry)
|
|
||||||
(equal (plist-get entry :background) "red")))
|
|
||||||
face))
|
|
||||||
(should
|
|
||||||
(cl-some (lambda (entry)
|
|
||||||
(and (listp entry)
|
|
||||||
(equal (plist-get entry :foreground) "blue")))
|
|
||||||
face))))
|
|
||||||
(ert-deftest ebox-build-gives-equal-forms-distinct-source-identities ()
|
(ert-deftest ebox-build-gives-equal-forms-distinct-source-identities ()
|
||||||
"Structurally equal author forms must not share opaque source identity."
|
"Structurally equal author forms must not share opaque source identity."
|
||||||
(let* ((node (ebox-build '(row (box "same") (box "same"))))
|
(let* ((node (ebox-build '(row (box "same") (box "same"))))
|
||||||
|
|||||||
@ -244,8 +244,8 @@
|
|||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
(ert-deftest ebox-selector-buffer-index-uses-logical-not-adapter-paths ()
|
(ert-deftest ebox-selector-buffer-index-preserves-author-form-types ()
|
||||||
"Indexed descendant queries should agree with the logical tree query."
|
"Indexed queries should preserve typed author-form selector subjects."
|
||||||
(ebox-selector-test--reset-runtime-state)
|
(ebox-selector-test--reset-runtime-state)
|
||||||
(let* ((node (ebox-build
|
(let* ((node (ebox-build
|
||||||
'(column :id shell
|
'(column :id shell
|
||||||
@ -254,23 +254,28 @@
|
|||||||
buffer)
|
buffer)
|
||||||
(unwind-protect
|
(unwind-protect
|
||||||
(progn
|
(progn
|
||||||
(should-not (ebox-selector-query-all node "column .target"))
|
(should (= (length (ebox-selector-query-all
|
||||||
|
node "column .target"))
|
||||||
|
1))
|
||||||
(setq buffer (ebox-render-to-buffer
|
(setq buffer (ebox-render-to-buffer
|
||||||
(generate-new-buffer-name " *ebox-selector-test*")
|
(generate-new-buffer-name " *ebox-selector-test*")
|
||||||
node))
|
node))
|
||||||
(let ((state (gethash buffer ebox--buffer-render-state-table)))
|
(let ((state (gethash buffer ebox--buffer-render-state-table)))
|
||||||
(should-not
|
(should (= (gethash
|
||||||
(gethash 'column (plist-get state :selector-type-table)))
|
'column
|
||||||
|
(plist-get state :runtime-type-count-table))
|
||||||
|
1))
|
||||||
(should (= (gethash
|
(should (= (gethash
|
||||||
'box
|
'box
|
||||||
(plist-get state :runtime-type-count-table))
|
(plist-get state :runtime-type-count-table))
|
||||||
3))
|
2))
|
||||||
(should (= (gethash
|
(should (= (gethash
|
||||||
'text
|
'text
|
||||||
(plist-get state :runtime-type-count-table))
|
(plist-get state :runtime-type-count-table))
|
||||||
2)))
|
2)))
|
||||||
(should-not (ebox-selector-query-buffer
|
(should (= (length (ebox-selector-query-buffer
|
||||||
buffer "column .target")))
|
buffer "column .target"))
|
||||||
|
1)))
|
||||||
(when (and buffer (buffer-live-p buffer))
|
(when (and buffer (buffer-live-p buffer))
|
||||||
(kill-buffer buffer)))))
|
(kill-buffer buffer)))))
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user