Add retained layer layout: position/left/top/z-index/layer/anchor properties, new ebox-layer.el and ebox-composite.el, update docs and Makefile
Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run
Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run
This commit is contained in:
parent
a8417d56c6
commit
4a25d573c2
6
Makefile
6
Makefile
@ -23,6 +23,12 @@ NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release
|
|||||||
|
|
||||||
all: check
|
all: check
|
||||||
|
|
||||||
|
.PHONY: layer-tests
|
||||||
|
layer-tests:
|
||||||
|
$(EMACS_TEST) -l tests/ebox-composite-tests.el -l tests/ebox-layer-style-tests.el -l tests/ebox-layer-tests.el -l tests/ebox-layer-publication-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
test: layer-tests
|
||||||
|
|
||||||
interaction-tests:
|
interaction-tests:
|
||||||
$(EMACS_TEST) -l tests/ebox-interaction-tests.el -f ert-run-tests-batch-and-exit
|
$(EMACS_TEST) -l tests/ebox-interaction-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
|||||||
@ -74,6 +74,7 @@ another public node or wrapper.
|
|||||||
| Paint | `:color`, `:background-color`, exact alias `:bgcolor`, border colors, `:visibility` |
|
| Paint | `:color`, `:background-color`, exact alias `:bgcolor`, border colors, `:visibility` |
|
||||||
| Text layout | `:text-align`, `:wrap-mode` (`word`, `char`, `kp`, `none`) |
|
| Text layout | `:text-align`, `:wrap-mode` (`word`, `char`, `kp`, `none`) |
|
||||||
| Overflow | `:overflow` (`visible`, `hidden`, `scroll`) |
|
| Overflow | `:overflow` (`visible`, `hidden`, `scroll`) |
|
||||||
|
| Layer placement | `:position`, `:left`, `:top`, `:z-index`, `:layer`, `:anchor`, `:placement` |
|
||||||
|
|
||||||
`hidden` enforces the content width after composing children, preserving whole
|
`hidden` enforces the content width after composing children, preserving whole
|
||||||
supported text clusters and excluding the container's own padding and border
|
supported text clusters and excluding the container's own padding and border
|
||||||
@ -109,6 +110,53 @@ guide](ebox-user-guide.en.md#4-use-geometry-and-paint-properties) for percentage
|
|||||||
references, font units, arithmetic rules, and whole-line vertical display
|
references, font units, arithmetic rules, and whole-line vertical display
|
||||||
quantization. This is a CSS semantic subset, not a browser layout engine.
|
quantization. This is a CSS semantic subset, not a browser layout engine.
|
||||||
|
|
||||||
|
### Retained layer placement
|
||||||
|
|
||||||
|
These non-inherited geometry properties apply to `box`, `row`, `column`,
|
||||||
|
`flex`, and `grid`; Text does not accept them. Existing author tags are unchanged.
|
||||||
|
|
||||||
|
| Property | Accepted value | Default |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `:position` | `static`, `relative`, `absolute` | `static` |
|
||||||
|
| `:left` | Signed inline length (`px`, `ch`, `vw`, `%`) or size expression | `(px 0)` |
|
||||||
|
| `:top` | Signed block length (`lh`, `vh`, `%`) or size expression | `(lh 0)` |
|
||||||
|
| `:z-index` | Integer, including negative values | `0` |
|
||||||
|
| `:layer` | `local`, `root` | `local` |
|
||||||
|
| `:anchor` | Semantic `:id` as a symbol or string, or `nil` | `nil` |
|
||||||
|
| `:placement` | `bottom-start`, `bottom-end`, `top-start`, `top-end` | `bottom-start` |
|
||||||
|
|
||||||
|
`static` uses ordinary flow. `relative` keeps its flow slot and shifts its paint
|
||||||
|
by `:left` and `:top`. `absolute` contributes no normal-flow size or slot and
|
||||||
|
uses the parent content origin, or the root content canvas for `:layer root`.
|
||||||
|
Offsets use that paint host's font and containing dimensions; `%` uses width
|
||||||
|
for `:left` and height for `:top`. Fractions survive length resolution; the
|
||||||
|
final vertical offset is floored to the host's whole text rows.
|
||||||
|
|
||||||
|
Each local host composes its children as one group. Within a group, smaller
|
||||||
|
`:z-index` values paint first; at equal depth, normal content paints before
|
||||||
|
positioned content, and positioned siblings follow logical document order.
|
||||||
|
A nested child's depth does not escape its local group. A painted Box is
|
||||||
|
opaque across its content, padding, and border, including blank space; its
|
||||||
|
margin is excluded. Paint is clipped to the host canvas.
|
||||||
|
|
||||||
|
`:layer root` changes the paint target while preserving the logical parent,
|
||||||
|
selectors, inheritance, and retained identity. Both `:layer root` and a non-nil
|
||||||
|
`:anchor` require `:position absolute`. Anchors resolve a unique semantic ID in
|
||||||
|
the paint host's logical subtree and use its visible bounds. The panel starts
|
||||||
|
above/below and at the start/end edge as requested, flips vertically when the
|
||||||
|
opposite side fits, then shifts inside the canvas. Missing or invisible anchors
|
||||||
|
produce no panel paint; ambiguous IDs and cyclic placement dependencies signal
|
||||||
|
an error before publication.
|
||||||
|
|
||||||
|
This is an opaque text compositor: horizontal positions are pixels, vertical
|
||||||
|
positions share the host's row grid. Positioned descendants must share their
|
||||||
|
host's effective font line height. Whole supported text clusters and display
|
||||||
|
units survive clipping; partial glyphs/images become neutral space. There is
|
||||||
|
no alpha blending, partial-glyph masking, or child-frame rendering. Layered
|
||||||
|
trees use Elisp when the native representation cannot express their placement.
|
||||||
|
See the [layer guide](ebox-user-guide.en.md#retained-layers) for examples and
|
||||||
|
retained update behavior.
|
||||||
|
|
||||||
### Native node capabilities
|
### Native node capabilities
|
||||||
|
|
||||||
`text`, `box`, `row`, `column`, `flex`, and `grid` accept these four explicit
|
`text`, `box`, `row`, `column`, `flex`, and `grid` accept these four explicit
|
||||||
|
|||||||
@ -69,6 +69,7 @@ participation property 就是普通子 Box property,不会创建另一种公
|
|||||||
| 绘制 | `:color`、`:background-color`、精确 alias `:bgcolor`、border color、`:visibility` |
|
| 绘制 | `:color`、`:background-color`、精确 alias `:bgcolor`、border color、`:visibility` |
|
||||||
| 文本布局 | `:text-align`、`:wrap-mode`(`word`、`char`、`kp`、`none`) |
|
| 文本布局 | `:text-align`、`:wrap-mode`(`word`、`char`、`kp`、`none`) |
|
||||||
| Overflow | `:overflow`(`visible`、`hidden`、`scroll`) |
|
| Overflow | `:overflow`(`visible`、`hidden`、`scroll`) |
|
||||||
|
| 图层放置 | `:position`、`:left`、`:top`、`:z-index`、`:layer`、`:anchor`、`:placement` |
|
||||||
|
|
||||||
`hidden` 在组合子节点后约束内容宽度,保留完整的受支持文本簇;容器自身的 padding 与
|
`hidden` 在组合子节点后约束内容宽度,保留完整的受支持文本簇;容器自身的 padding 与
|
||||||
border 不在裁剪区域内。被裁掉的子文本同时移除其属性。固定 display space 可以缩短,
|
border 不在裁剪区域内。被裁掉的子文本同时移除其属性。固定 display space 可以缩短,
|
||||||
@ -95,6 +96,43 @@ context/value 组合无效时会直接报错。百分比参照、字体单位、
|
|||||||
[用户指南的几何章节](ebox-user-guide.zh.md#4-使用几何与绘制-property)。这是 CSS
|
[用户指南的几何章节](ebox-user-guide.zh.md#4-使用几何与绘制-property)。这是 CSS
|
||||||
语义的子集,不是完整浏览器布局引擎。
|
语义的子集,不是完整浏览器布局引擎。
|
||||||
|
|
||||||
|
### 保留式图层放置
|
||||||
|
|
||||||
|
以下不继承的几何属性适用于 `box`、`row`、`column`、`flex` 和 `grid`,Text
|
||||||
|
不接受这些属性。现有 author tag 不变。
|
||||||
|
|
||||||
|
| 属性 | 接受的值 | 默认值 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `:position` | `static`、`relative`、`absolute` | `static` |
|
||||||
|
| `:left` | 有符号 inline 长度(`px`、`ch`、`vw`、`%`)或尺寸表达式 | `(px 0)` |
|
||||||
|
| `:top` | 有符号 block 长度(`lh`、`vh`、`%`)或尺寸表达式 | `(lh 0)` |
|
||||||
|
| `:z-index` | 整数,允许负值 | `0` |
|
||||||
|
| `:layer` | `local`、`root` | `local` |
|
||||||
|
| `:anchor` | 以 symbol 或字符串表示的语义 `:id`,或 `nil` | `nil` |
|
||||||
|
| `:placement` | `bottom-start`、`bottom-end`、`top-start`、`top-end` | `bottom-start` |
|
||||||
|
|
||||||
|
`static` 使用普通流。`relative` 保留流中占位,通过 `:left`、`:top` 平移绘制。
|
||||||
|
`absolute` 不贡献普通流尺寸或占位,以父容器内容原点为基准;`:layer root` 则使用
|
||||||
|
根内容画布。偏移使用绘制宿主的字体与包含尺寸;`:left` 的 `%` 参照宽度,`:top`
|
||||||
|
的 `%` 参照高度。长度解析保留小数,最终纵向偏移向下取整到宿主的完整文本行。
|
||||||
|
|
||||||
|
每个局部宿主把子节点合成为一个组。组内较小的 `:z-index` 先绘制;深度相同时,
|
||||||
|
普通内容先于定位内容绘制,定位兄弟按逻辑文档顺序绘制。嵌套子节点的深度不能
|
||||||
|
越过局部分组。Box 的内容、padding 和 border 区域均为不透明绘制,空白也会遮挡;
|
||||||
|
margin 不参与遮挡。绘制裁剪到宿主画布。
|
||||||
|
|
||||||
|
`:layer root` 只改变绘制目标,保留逻辑父节点、selector、继承和 retained identity。
|
||||||
|
`:layer root` 与非 nil 的 `:anchor` 都要求 `:position absolute`。anchor 在绘制
|
||||||
|
宿主的逻辑子树中解析唯一的语义 ID,并使用目标的可见边界。面板按指定的上下及
|
||||||
|
起止边放置,对侧可容纳时纵向翻转,然后平移到画布内。anchor 缺失或不可见时不
|
||||||
|
绘制面板;ID 歧义或循环放置依赖在发布前报错。
|
||||||
|
|
||||||
|
这是不透明文本合成:横向用像素,纵向共享宿主的文本行网格。定位子树必须与宿主
|
||||||
|
使用相同的有效字体行高。裁剪保留完整的受支持文本簇和 display unit;不完整字形或
|
||||||
|
图像以中性空白替代。不提供 alpha 混合、半字形遮罩或 child frame。native 表示
|
||||||
|
无法表达图层放置时使用 Elisp。示例和 retained 更新行为见
|
||||||
|
[图层指南](ebox-user-guide.zh.md#retained-layers)。
|
||||||
|
|
||||||
### 原生节点能力
|
### 原生节点能力
|
||||||
|
|
||||||
`text`、`box`、`row`、`column`、`flex` 和 `grid` 都接受下面四个显式节点属性。
|
`text`、`box`、`row`、`column`、`flex` 和 `grid` 都接受下面四个显式节点属性。
|
||||||
|
|||||||
@ -171,6 +171,7 @@ mode. These axis restrictions deliberately narrow CSS's general length model.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Inline geometry | `px`, `ch`, `vw`, `%` | `width`, `min-width`, `max-width`; left/right and inline padding/margin; `column-gap`; Row `item-gap`; Grid column tracks. |
|
| Inline geometry | `px`, `ch`, `vw`, `%` | `width`, `min-width`, `max-width`; left/right and inline padding/margin; `column-gap`; Row `item-gap`; Grid column tracks. |
|
||||||
| Block geometry | `lh`, `vh`, `%` | `height`, `min-height`, `max-height`; top/bottom and block padding/margin; `row-gap`; Column `item-gap`; Grid row tracks. |
|
| Block geometry | `lh`, `vh`, `%` | `height`, `min-height`, `max-height`; top/bottom and block padding/margin; `row-gap`; Column `item-gap`; Grid row tracks. |
|
||||||
|
| Positioned offsets | Inline units for `left`; block units for `top` | Signed lengths and size expressions; `%` refers to the paint host's corresponding content axis. |
|
||||||
| Flex main size | The parent Flex's main-axis units | `flex-basis` and the basis in `(grow shrink basis)`; row directions use inline units, column directions use block units. |
|
| Flex main size | The parent Flex's main-axis units | `flex-basis` and the basis in `(grow shrink basis)`; row directions use inline units, column directions use block units. |
|
||||||
| Border paint thickness | `px`, `ch`, `lh`, `vw`, `vh` | `border-width`, each side's border width, and the width component of border shorthands; percentages are forbidden. |
|
| Border paint thickness | `px`, `ch`, `lh`, `vw`, `vh` | `border-width`, each side's border width, and the width component of border shorthands; percentages are forbidden. |
|
||||||
|
|
||||||
@ -280,6 +281,79 @@ Text Box geometry.
|
|||||||
Use `:outer inline` or `:outer block` to state how a Box participates in its
|
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.
|
parent. The child-layout algorithm still comes from the form name.
|
||||||
|
|
||||||
|
<a id="retained-layers"></a>
|
||||||
|
|
||||||
|
### Retained layers
|
||||||
|
|
||||||
|
Use positioning properties on existing Box forms to overlap content. Give the
|
||||||
|
host an explicit height when absolute children need a reserved canvas: they do
|
||||||
|
not increase its automatic flow size.
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-build
|
||||||
|
'(box :width (ch 12) :height (lh 3)
|
||||||
|
"ABCDEFGHIJKL\nabcdefghijkl\n0123456789ab"
|
||||||
|
(box :position absolute :left (ch 2) :top (lh 1)
|
||||||
|
:width (ch 5) :height (lh 1)
|
||||||
|
:background-color "#334155" :color "#FFFFFF"
|
||||||
|
"Panel")))
|
||||||
|
```
|
||||||
|
|
||||||
|
`:position relative` keeps the child's ordinary slot and moves only its paint.
|
||||||
|
`:position absolute` removes the child from normal, Row, Column, Flex, or Grid
|
||||||
|
flow and places it from the parent content origin. Signed `:left` uses inline
|
||||||
|
units; signed `:top` uses block units and is floored to whole host rows at final
|
||||||
|
placement. Both accept size expressions. A local host clips its composed output
|
||||||
|
to its canvas. Nested local groups are automatic: a child's high `:z-index`
|
||||||
|
cannot jump above a sibling of its host. Integer depths paint low to high;
|
||||||
|
normal content precedes positioned content at equal depth, and equal-depth
|
||||||
|
positioned boxes follow document order.
|
||||||
|
|
||||||
|
The upper box covers its content, padding, and border, including blanks, while
|
||||||
|
its margin leaves the lower content visible. Visible text retains its native
|
||||||
|
help, pointer, hover, and keymap properties; covered text contributes none of
|
||||||
|
those interactions. The retained lower node and its semantic ID still exist.
|
||||||
|
Updates to it become visible with their latest content and properties when
|
||||||
|
the covering box moves or becomes hidden.
|
||||||
|
|
||||||
|
Use a root layer to place a panel beyond a clipped local container while
|
||||||
|
keeping its logical parent and inherited styles:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-build
|
||||||
|
'(box :width (ch 20) :height (lh 4)
|
||||||
|
(box :height (lh 1) :overflow hidden
|
||||||
|
(box :id "trigger" :width (ch 6) "Open")
|
||||||
|
(box :id "panel" :position absolute :layer root
|
||||||
|
:anchor "trigger" :placement bottom-start
|
||||||
|
:width (ch 8) :height (lh 2)
|
||||||
|
"Choice A\nChoice B"))))
|
||||||
|
```
|
||||||
|
|
||||||
|
`:layer root` selects the root content canvas as the paint target. It does not
|
||||||
|
reparent the panel for selectors, inheritance, or region updates. `:anchor`
|
||||||
|
uses a unique semantic ID in the paint host's logical subtree. Both properties
|
||||||
|
require absolute positioning. Placements are `bottom-start`, `bottom-end`,
|
||||||
|
`top-start`, and `top-end`; offsets adjust the requested origin. Panels flip
|
||||||
|
vertically if the opposite side fits, then shift within the host canvas.
|
||||||
|
Missing or invisible anchors do not paint a panel; ambiguous IDs and placement
|
||||||
|
dependency cycles are errors.
|
||||||
|
|
||||||
|
Composition uses horizontal pixels and one shared vertical text-row grid.
|
||||||
|
Positioned subtrees must have the same effective font line height as their
|
||||||
|
host. Whole supported clusters survive cuts; partially covered glyphs/images
|
||||||
|
are replaced with neutral space, and fixed display spaces can be shortened.
|
||||||
|
There is no alpha blending, half-glyph masking, or child frame. Layered trees
|
||||||
|
fall back to Elisp when native reflow cannot represent them.
|
||||||
|
|
||||||
|
Publication remains a TP transaction, including hidden-node updates and
|
||||||
|
rollback. Fixed-footprint local changes can recompose only the affected host;
|
||||||
|
geometry changes and root-layer dependencies may require broader work. No
|
||||||
|
per-update latency or universal local-update guarantee is implied. Run
|
||||||
|
`make layer-tests` for the focused contract. The separate `ebox-playground`
|
||||||
|
package provides `layer-minimal.ebox` and `layer-reference.ebox`; batch tests
|
||||||
|
do not establish GUI visual parity or remote CI success.
|
||||||
|
|
||||||
### Wrapping and the optional EKP dependency
|
### Wrapping and the optional EKP dependency
|
||||||
|
|
||||||
`:wrap-mode word` wraps ordinary words and permits character breaks for CJK;
|
`:wrap-mode word` wraps ordinary words and permits character breaks for CJK;
|
||||||
|
|||||||
@ -152,6 +152,7 @@ Flex basis 和 Grid 固定轨道都使用这套格式。Ebox 实现 CSS 尺寸
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Inline 几何 | `px`、`ch`、`vw`、`%` | `width`、`min-width`、`max-width`;左右和 inline padding/margin;`column-gap`;Row 的 `item-gap`;Grid 列轨道。 |
|
| Inline 几何 | `px`、`ch`、`vw`、`%` | `width`、`min-width`、`max-width`;左右和 inline padding/margin;`column-gap`;Row 的 `item-gap`;Grid 列轨道。 |
|
||||||
| Block 几何 | `lh`、`vh`、`%` | `height`、`min-height`、`max-height`;上下和 block padding/margin;`row-gap`;Column 的 `item-gap`;Grid 行轨道。 |
|
| Block 几何 | `lh`、`vh`、`%` | `height`、`min-height`、`max-height`;上下和 block padding/margin;`row-gap`;Column 的 `item-gap`;Grid 行轨道。 |
|
||||||
|
| 定位偏移 | `left` 使用 inline 单位;`top` 使用 block 单位 | 有符号长度及尺寸表达式;`%` 参照绘制宿主对应内容轴。 |
|
||||||
| Flex 主轴尺寸 | 所在父 Flex 主轴允许的单位 | `flex-basis` 及 `(grow shrink basis)` 中的 basis;row 方向用 inline 单位,column 方向用 block 单位。 |
|
| Flex 主轴尺寸 | 所在父 Flex 主轴允许的单位 | `flex-basis` 及 `(grow shrink basis)` 中的 basis;row 方向用 inline 单位,column 方向用 block 单位。 |
|
||||||
| 边框绘制厚度 | `px`、`ch`、`lh`、`vw`、`vh` | `border-width`、各侧边框宽度、border 简写中的宽度;不接受百分比。 |
|
| 边框绘制厚度 | `px`、`ch`、`lh`、`vw`、`vh` | `border-width`、各侧边框宽度、border 简写中的宽度;不接受百分比。 |
|
||||||
|
|
||||||
@ -242,6 +243,65 @@ Text 的样式声明只接受字体、前景/背景和文本装饰 property;
|
|||||||
用 `:outer inline` 或 `:outer block` 表达 Box 如何参与父布局。子布局算法仍由 form
|
用 `:outer inline` 或 `:outer block` 表达 Box 如何参与父布局。子布局算法仍由 form
|
||||||
名称决定。
|
名称决定。
|
||||||
|
|
||||||
|
<a id="retained-layers"></a>
|
||||||
|
|
||||||
|
### 保留式图层
|
||||||
|
|
||||||
|
在现有 Box form 上设置定位属性即可重叠内容。绝对定位子节点不增加普通流的自动
|
||||||
|
尺寸,需要预留画布时,为宿主声明明确的高度。
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-build
|
||||||
|
'(box :width (ch 12) :height (lh 3)
|
||||||
|
"ABCDEFGHIJKL\nabcdefghijkl\n0123456789ab"
|
||||||
|
(box :position absolute :left (ch 2) :top (lh 1)
|
||||||
|
:width (ch 5) :height (lh 1)
|
||||||
|
:background-color "#334155" :color "#FFFFFF"
|
||||||
|
"Panel")))
|
||||||
|
```
|
||||||
|
|
||||||
|
`:position relative` 保留普通流中的占位,只移动绘制。`:position absolute`
|
||||||
|
从 normal、Row、Column、Flex 或 Grid 流中移除子节点,并从父内容原点放置。
|
||||||
|
有符号 `:left` 使用 inline 单位;有符号 `:top` 使用 block 单位,最终放置时
|
||||||
|
向下取整到宿主的完整行。两者都接受尺寸表达式。局部宿主把合成输出裁剪到自身
|
||||||
|
画布。嵌套局部分组自动建立:子节点再高的 `:z-index` 也无法越过宿主的兄弟。
|
||||||
|
整数深度从低到高绘制;同深度的普通内容先于定位内容,定位节点按文档顺序绘制。
|
||||||
|
|
||||||
|
上层 Box 的内容、padding 和 border 都遮挡下层,空白也不例外;margin 则保留
|
||||||
|
下层可见性。可见文本保留原生 help、pointer、hover 和 keymap 属性,被覆盖的
|
||||||
|
文本不贡献这些交互。下层 retained 节点及语义 ID 仍然存在;遮挡期间更新后,
|
||||||
|
上层移动或隐藏时会显示下层最新的内容与属性。
|
||||||
|
|
||||||
|
使用根图层可把面板绘制到局部裁剪容器之外,同时保留逻辑父节点和继承样式:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-build
|
||||||
|
'(box :width (ch 20) :height (lh 4)
|
||||||
|
(box :height (lh 1) :overflow hidden
|
||||||
|
(box :id "trigger" :width (ch 6) "Open")
|
||||||
|
(box :id "panel" :position absolute :layer root
|
||||||
|
:anchor "trigger" :placement bottom-start
|
||||||
|
:width (ch 8) :height (lh 2)
|
||||||
|
"Choice A\nChoice B"))))
|
||||||
|
```
|
||||||
|
|
||||||
|
`:layer root` 选择根内容画布作为绘制目标,不改变 selector、继承或 region 更新
|
||||||
|
所依据的父子关系。`:anchor` 使用绘制宿主逻辑子树中的唯一语义 ID;两者都要求
|
||||||
|
绝对定位。placement 支持 `bottom-start`、`bottom-end`、`top-start`、`top-end`,
|
||||||
|
偏移调整请求的原点。对侧能容纳时,面板纵向翻转,再平移到宿主画布内。anchor
|
||||||
|
缺失或不可见时不绘制面板;ID 歧义及放置依赖循环会报错。
|
||||||
|
|
||||||
|
合成横向使用像素,纵向共享一套文本行网格。定位子树的有效字体行高必须与宿主
|
||||||
|
相同。裁剪保留完整的受支持文本簇,不完整字形或图像以中性空白替代,固定 display
|
||||||
|
space 可以缩短。不提供 alpha 混合、半字形遮罩或 child frame。native reflow
|
||||||
|
无法表示图层时回退到 Elisp。
|
||||||
|
|
||||||
|
包括隐藏节点更新和 rollback 在内的发布仍通过 TP transaction。固定外部尺寸的
|
||||||
|
局部修改可以只重新合成受影响的宿主;几何变化或根图层依赖可能需要扩大范围。
|
||||||
|
不保证每次更新的耗时或所有更新都保持局部。用 `make layer-tests` 运行聚焦契约。
|
||||||
|
独立的 `ebox-playground` 包提供 `layer-minimal.ebox`、`layer-reference.ebox`;
|
||||||
|
batch 测试不证明 GUI 视觉一致性或远程 CI 成功。
|
||||||
|
|
||||||
### 换行与可选 EKP 依赖
|
### 换行与可选 EKP 依赖
|
||||||
|
|
||||||
`:wrap-mode word` 按普通单词换行,并允许 CJK 字符换行;`char` 按 Ebox 支持的常见
|
`:wrap-mode word` 按普通单词换行,并允许 CJK 字符换行;`char` 按 Ebox 支持的常见
|
||||||
|
|||||||
216
ebox-composite.el
Normal file
216
ebox-composite.el
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
;;; ebox-composite.el --- Pixel interval row composition -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Compose opaque, already rendered rows without changing their source strings.
|
||||||
|
;; Resolve coverage first so every visible interval is cut from its original
|
||||||
|
;; source, independent of the number or ordering of hidden lower placements.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ebox-measure)
|
||||||
|
|
||||||
|
(declare-function ebox--grapheme-cluster-end "ebox-layout" (string start))
|
||||||
|
|
||||||
|
(defun ebox-composite--line-p (line)
|
||||||
|
"Return non-nil if LINE is a single-line string or nil."
|
||||||
|
(or (null line)
|
||||||
|
(and (stringp line) (not (string-match-p "\n" line)))))
|
||||||
|
|
||||||
|
(defun ebox-composite--replacement-p (display)
|
||||||
|
"Return non-nil when DISPLAY replaces an entire EQ property run."
|
||||||
|
(or (stringp display) (memq (car-safe display) '(space image))))
|
||||||
|
|
||||||
|
(defun ebox-composite--unit-end (line position)
|
||||||
|
"Return the complete display unit end in LINE at POSITION."
|
||||||
|
(let ((end (ebox--grapheme-cluster-end line position)))
|
||||||
|
(if (ebox-composite--replacement-p
|
||||||
|
(get-text-property position 'display line))
|
||||||
|
(max end (next-single-property-change
|
||||||
|
position 'display line (length line)))
|
||||||
|
end)))
|
||||||
|
|
||||||
|
(defun ebox-composite--space (line start end width)
|
||||||
|
"Copy LINE's fixed space at START through END using WIDTH pixels.
|
||||||
|
Retain the source properties and nested display values by identity."
|
||||||
|
(let* ((result (substring line start end))
|
||||||
|
(display (get-text-property start 'display line))
|
||||||
|
(properties (copy-sequence (cdr display))))
|
||||||
|
(put-text-property
|
||||||
|
0 (length result) 'display
|
||||||
|
(cons 'space (plist-put properties :width (list width))) result)
|
||||||
|
result))
|
||||||
|
|
||||||
|
(defun ebox-composite--join (parts)
|
||||||
|
"Join PARTS while retaining their separately measured pixel extents.
|
||||||
|
A neutral zero-pixel display boundary prevents adjacent EQ replacement runs
|
||||||
|
from collapsing or independently shaped source fragments from reshaping."
|
||||||
|
(let ((result "") (width 0))
|
||||||
|
(dolist (part parts result)
|
||||||
|
(unless (equal part "")
|
||||||
|
(let* ((next-width (+ width (ebox--string-pixel-width part)))
|
||||||
|
(joined (concat result part)))
|
||||||
|
(setq result
|
||||||
|
(if (= (ebox--string-pixel-width joined) next-width)
|
||||||
|
joined
|
||||||
|
(concat result
|
||||||
|
(propertize " " 'display '(space :width (0)))
|
||||||
|
part))
|
||||||
|
width next-width))))))
|
||||||
|
|
||||||
|
(defun ebox-composite--fit (line start end left right)
|
||||||
|
"Fit LINE from START through END in its original [LEFT, RIGHT) pixels.
|
||||||
|
Removing a left shaping context can change a retained unit's width. Resolve
|
||||||
|
that unit within its original pixel interval, then retry the remaining whole
|
||||||
|
run. A newly wider glyph becomes neutral fill rather than shifting the next
|
||||||
|
source glyph or borrowing its pixels."
|
||||||
|
(let ((position start) (pixel left) parts)
|
||||||
|
(while (< position end)
|
||||||
|
(let* ((remaining (substring line position end))
|
||||||
|
(width (ebox--string-pixel-width remaining)))
|
||||||
|
(if (= width (- right pixel))
|
||||||
|
(progn (push remaining parts) (setq position end))
|
||||||
|
(let* ((next (ebox-composite--unit-end line position))
|
||||||
|
(next-pixel (ebox--substring-pixel-width line 0 next))
|
||||||
|
(unit (substring line position next))
|
||||||
|
(unit-width (ebox--string-pixel-width unit))
|
||||||
|
(available (- next-pixel pixel)))
|
||||||
|
(cond
|
||||||
|
((<= unit-width available)
|
||||||
|
(push unit parts)
|
||||||
|
(push (ebox-pixel-space (- available unit-width)) parts))
|
||||||
|
((ebox--display-space-width
|
||||||
|
(get-text-property position 'display line))
|
||||||
|
(push (ebox-composite--space line position next available)
|
||||||
|
parts))
|
||||||
|
(t (push (ebox-pixel-space available) parts)))
|
||||||
|
(setq position next pixel next-pixel)))))
|
||||||
|
(ebox-composite--join (nreverse parts))))
|
||||||
|
|
||||||
|
(defun ebox-composite-slice (line start end)
|
||||||
|
"Extract LINE's pixel interval [START, END), exactly END minus START wide.
|
||||||
|
START and END must be finite nonnegative numbers with START <= END. LINE is
|
||||||
|
a single-line string or nil. Pixels beyond LINE are neutral blank space.
|
||||||
|
Keep complete supported graphemes and replacement display runs, preserving
|
||||||
|
their text property values by identity. Fixed pixel spaces can be cut at
|
||||||
|
either edge; other partial glyphs become neutral blanks at their original
|
||||||
|
positions. Measure original prefixes so adjacent shaping is not summed as
|
||||||
|
isolated characters. Neither LINE nor its property values are modified."
|
||||||
|
(unless (ebox-composite--line-p line)
|
||||||
|
(error "Ebox composite needs a single-line string: %S" line))
|
||||||
|
(unless (and (ebox-size--finite-number-p start) (>= start 0)
|
||||||
|
(ebox-size--finite-number-p end) (>= end start))
|
||||||
|
(error "Invalid Ebox composite pixel interval: %S %S" start end))
|
||||||
|
(let* ((line (or line ""))
|
||||||
|
(line-width (ebox--string-pixel-width line)))
|
||||||
|
(cond
|
||||||
|
((= start end) "")
|
||||||
|
((and (= start 0) (<= line-width end))
|
||||||
|
(ebox-composite--join
|
||||||
|
(list line (ebox-pixel-space (- end line-width)))))
|
||||||
|
((>= start line-width) (ebox-pixel-space (- end start)))
|
||||||
|
(t
|
||||||
|
(let ((position 0) (pixel 0) parts
|
||||||
|
run-start run-end run-left run-right)
|
||||||
|
(cl-labels
|
||||||
|
((flush ()
|
||||||
|
(when run-start
|
||||||
|
(push (ebox-composite--fit
|
||||||
|
line run-start run-end run-left run-right)
|
||||||
|
parts)
|
||||||
|
(setq run-start nil))))
|
||||||
|
(while (and (< position (length line)) (< pixel end))
|
||||||
|
(let* ((next (ebox-composite--unit-end line position))
|
||||||
|
(next-pixel (ebox--substring-pixel-width line 0 next))
|
||||||
|
(left (max start pixel))
|
||||||
|
(right (min end next-pixel)))
|
||||||
|
(cond
|
||||||
|
((and (>= pixel start) (<= next-pixel end))
|
||||||
|
(unless run-start
|
||||||
|
(setq run-start position run-left pixel))
|
||||||
|
(setq run-end next run-right next-pixel))
|
||||||
|
((< left right)
|
||||||
|
(flush)
|
||||||
|
(push (if (ebox--display-space-width
|
||||||
|
(get-text-property position 'display line))
|
||||||
|
(ebox-composite--space
|
||||||
|
line position next (- right left))
|
||||||
|
(ebox-pixel-space (- right left)))
|
||||||
|
parts)))
|
||||||
|
(setq position next pixel next-pixel)))
|
||||||
|
(flush))
|
||||||
|
(when (> end (max start pixel))
|
||||||
|
(push (ebox-pixel-space (- end (max start pixel))) parts))
|
||||||
|
(ebox-composite--join (nreverse parts)))))))
|
||||||
|
|
||||||
|
(defun ebox-composite--uncovered (start end covered)
|
||||||
|
"Return portions of [START, END) outside sorted merged COVERED intervals."
|
||||||
|
(let ((cursor start) result)
|
||||||
|
(dolist (interval covered)
|
||||||
|
(when (and (< cursor end) (> (cdr interval) cursor))
|
||||||
|
(when (> (car interval) cursor)
|
||||||
|
(push (cons cursor (min end (car interval))) result))
|
||||||
|
(setq cursor (max cursor (cdr interval)))))
|
||||||
|
(when (< cursor end) (push (cons cursor end) result))
|
||||||
|
(nreverse result)))
|
||||||
|
|
||||||
|
(defun ebox-composite--cover (start end covered)
|
||||||
|
"Return sorted merged COVERED intervals including [START, END).
|
||||||
|
Do not modify the input interval list or its cons cells."
|
||||||
|
(let (result)
|
||||||
|
(dolist (interval covered)
|
||||||
|
(cond
|
||||||
|
((< (cdr interval) start) (push interval result))
|
||||||
|
((> (car interval) end)
|
||||||
|
(push (cons start end) result)
|
||||||
|
(setq start (car interval) end (cdr interval)))
|
||||||
|
(t (setq start (min start (car interval))
|
||||||
|
end (max end (cdr interval))))))
|
||||||
|
(push (cons start end) result)
|
||||||
|
(nreverse result)))
|
||||||
|
|
||||||
|
(defun ebox-composite-line (base placements width)
|
||||||
|
"Compose BASE and opaque PLACEMENTS on a fixed WIDTH-pixel canvas.
|
||||||
|
BASE is a single-line string or nil. PLACEMENTS is a proper list of plists
|
||||||
|
with a single-line string :text and finite pixel :x, already ordered bottom
|
||||||
|
to top. Each placement covers the full measured width of its text, including
|
||||||
|
blanks. Negative and overflowing placements are clipped to the canvas.
|
||||||
|
WIDTH must be finite and nonnegative. Uncovered pixels beyond BASE receive
|
||||||
|
neutral fill. Compute coverage before extracting original source intervals,
|
||||||
|
so hidden placements cannot introduce cuts into a visible source glyph.
|
||||||
|
Do not modify BASE, PLACEMENTS, or any of their text property values."
|
||||||
|
(unless (and (ebox-composite--line-p base)
|
||||||
|
(proper-list-p placements)
|
||||||
|
(ebox-size--finite-number-p width) (>= width 0))
|
||||||
|
(error "Invalid Ebox composite canvas"))
|
||||||
|
(let (sources covered visible)
|
||||||
|
(dolist (placement placements)
|
||||||
|
(let ((text (plist-get placement :text))
|
||||||
|
(x (plist-get placement :x)))
|
||||||
|
(unless (and (stringp text) (ebox-composite--line-p text)
|
||||||
|
(ebox-size--finite-number-p x))
|
||||||
|
(error "Invalid Ebox composite placement: %S" placement))
|
||||||
|
(let* ((right (+ x (ebox--string-pixel-width text)))
|
||||||
|
(left (max 0 x))
|
||||||
|
(end (min width right)))
|
||||||
|
(unless (ebox-size--finite-number-p right)
|
||||||
|
(error "Ebox composite placement extent is not finite"))
|
||||||
|
(when (< left end)
|
||||||
|
(push (list left end text x) sources)))))
|
||||||
|
;; Sources were pushed from bottom to top. Only original source metadata
|
||||||
|
;; participates in occlusion; no sliced text is ever sliced again.
|
||||||
|
(dolist (source (append sources (list (list 0 width base 0))))
|
||||||
|
(pcase-let ((`(,start ,end ,text ,x) source))
|
||||||
|
(dolist (interval (ebox-composite--uncovered start end covered))
|
||||||
|
(push (list (car interval) (cdr interval) text x) visible))
|
||||||
|
(when (< start end)
|
||||||
|
(setq covered (ebox-composite--cover start end covered)))))
|
||||||
|
(ebox-composite--join
|
||||||
|
(mapcar
|
||||||
|
(lambda (interval)
|
||||||
|
(pcase-let ((`(,start ,end ,text ,x) interval))
|
||||||
|
(ebox-composite-slice text (- start x) (- end x))))
|
||||||
|
(sort visible (lambda (a b) (< (car a) (car b))))))))
|
||||||
|
|
||||||
|
(provide 'ebox-composite)
|
||||||
|
;;; ebox-composite.el ends here
|
||||||
@ -101,6 +101,9 @@
|
|||||||
"ebox-native-commit" (old-state candidate-state prepared))
|
"ebox-native-commit" (old-state candidate-state prepared))
|
||||||
(declare-function ebox-native-reflow-release-session
|
(declare-function ebox-native-reflow-release-session
|
||||||
"ebox-native-reflow" (session))
|
"ebox-native-reflow" (session))
|
||||||
|
(declare-function ebox-layer-host-p "ebox-layer" (node))
|
||||||
|
(declare-function ebox-layer-subtree-p "ebox-layer" (node))
|
||||||
|
(declare-function ebox-layer-portals-p "ebox-layer" (node))
|
||||||
|
|
||||||
(cl-defstruct
|
(cl-defstruct
|
||||||
(ebox-incremental-context-port
|
(ebox-incremental-context-port
|
||||||
@ -182,6 +185,9 @@
|
|||||||
(defvar ebox--render-cache-table nil
|
(defvar ebox--render-cache-table nil
|
||||||
"Dynamic buffer-local render cache used during incremental rerenders.")
|
"Dynamic buffer-local render cache used during incremental rerenders.")
|
||||||
|
|
||||||
|
(defvar ebox-incremental--render-portals-p 'unknown
|
||||||
|
"Whether the current candidate can produce root portal side effects.")
|
||||||
|
|
||||||
(defvar ebox--prepared-root-render nil
|
(defvar ebox--prepared-root-render nil
|
||||||
"Dynamically bound validated root render for one visible transaction.")
|
"Dynamically bound validated root render for one visible transaction.")
|
||||||
|
|
||||||
@ -1063,7 +1069,12 @@ EXTERNAL-SIGNATURE may prove a viewport-dependent non-root context."
|
|||||||
When FORCE is non-nil, cache NODE even when its output is viewport-dependent;
|
When FORCE is non-nil, cache NODE even when its output is viewport-dependent;
|
||||||
the complete viewport context remains part of the render signature.
|
the complete viewport context remains part of the render signature.
|
||||||
CACHE-PROBE may supply a prior exact miss from an accelerator boundary."
|
CACHE-PROBE may supply a prior exact miss from an accelerator boundary."
|
||||||
(if-let* ((prepared (and force
|
(if (and ebox-incremental--render-portals-p
|
||||||
|
(fboundp 'ebox-layer-portals-p)
|
||||||
|
(ebox-layer-portals-p node))
|
||||||
|
;; Cached strings do not replay the candidate-local portal collector.
|
||||||
|
(ebox--render-node node)
|
||||||
|
(if-let* ((prepared (and force
|
||||||
(ebox--take-prepared-root-render node))))
|
(ebox--take-prepared-root-render node))))
|
||||||
(prog1 prepared
|
(prog1 prepared
|
||||||
(ebox--replay-render-output-provenance prepared)
|
(ebox--replay-render-output-provenance prepared)
|
||||||
@ -1102,7 +1113,7 @@ CACHE-PROBE may supply a prior exact miss from an accelerator boundary."
|
|||||||
portable-p)))
|
portable-p)))
|
||||||
(ebox--render-cache-store-context probe rendered metadata)
|
(ebox--render-cache-store-context probe rendered metadata)
|
||||||
(ebox--record-render-output-provenance rendered))
|
(ebox--record-render-output-provenance rendered))
|
||||||
rendered))))))
|
rendered)))))))
|
||||||
|
|
||||||
;;; Buffer Runtime State
|
;;; Buffer Runtime State
|
||||||
|
|
||||||
@ -8606,6 +8617,100 @@ an owner for one descendant after its narrower projection proof has failed."
|
|||||||
(ebox-incremental--declarative-tentative-patch-set
|
(ebox-incremental--declarative-tentative-patch-set
|
||||||
buffer render-dirty-set)))))
|
buffer render-dirty-set)))))
|
||||||
|
|
||||||
|
(defun ebox-incremental--layer-owner-plan
|
||||||
|
(buffer old-state candidate-state dirty-set)
|
||||||
|
"Promote DIRTY-SET to complete compositing owners when layers are affected.
|
||||||
|
BUFFER owns OLD-STATE; CANDIDATE-STATE supplies the next logical tree.
|
||||||
|
Ordinary mounted trees use their retained negative layer flag. Introducing
|
||||||
|
layers only inspects changed declarations or replacement subtrees. Nested
|
||||||
|
hosts promote through enclosing hosts because their upper siblings may hide
|
||||||
|
the inner host's output."
|
||||||
|
(when (and (fboundp 'ebox-layer-host-p)
|
||||||
|
(or (plist-get old-state :layered-p)
|
||||||
|
(cl-some
|
||||||
|
(lambda (dirty)
|
||||||
|
(cl-intersection (plist-get dirty :changed-keys)
|
||||||
|
'(:position :layer :children)))
|
||||||
|
dirty-set)))
|
||||||
|
(let ((old-nodes (plist-get old-state :node-table))
|
||||||
|
(nodes (plist-get candidate-state :node-table))
|
||||||
|
(parents (plist-get candidate-state :parent-table))
|
||||||
|
(ebox-incremental--buffer-render-state-override
|
||||||
|
(cons buffer candidate-state))
|
||||||
|
affected operations)
|
||||||
|
(dolist (dirty dirty-set)
|
||||||
|
(unless (eq (plist-get dirty :dirty-kind) 'metadata)
|
||||||
|
(let ((node-id (plist-get dirty :node-id)) owner)
|
||||||
|
(while node-id
|
||||||
|
(when (or (ebox-layer-host-p
|
||||||
|
(ebox-runtime-index-get node-id nodes))
|
||||||
|
(ebox-layer-host-p
|
||||||
|
(ebox-runtime-index-get node-id old-nodes)))
|
||||||
|
(setq owner node-id))
|
||||||
|
(setq node-id (ebox-runtime-index-get node-id parents)))
|
||||||
|
;; Paint or layout on an ordinary ancestor of a layer also
|
||||||
|
;; requires fresh composition, rather than a blind paint patch.
|
||||||
|
(unless owner
|
||||||
|
(let ((node (ebox-runtime-index-get
|
||||||
|
(plist-get dirty :node-id) nodes)))
|
||||||
|
(when (and node (ebox-layer-subtree-p node))
|
||||||
|
(setq owner (plist-get dirty :node-id)))))
|
||||||
|
(push (if owner
|
||||||
|
(progn
|
||||||
|
(setq affected t)
|
||||||
|
(ebox--patch-op 'owner-rerender owner :dirty dirty
|
||||||
|
:layer-recompose-p t))
|
||||||
|
(ebox-incremental--declarative-tentative-op buffer dirty))
|
||||||
|
operations))))
|
||||||
|
;; A root portal can cover or anchor to nodes outside its local host.
|
||||||
|
;; Their updates therefore belong to the root composition as well.
|
||||||
|
(when (or affected
|
||||||
|
(and operations (plist-get old-state :root-portals-p)))
|
||||||
|
(if (or (plist-get old-state :root-portals-p)
|
||||||
|
(ebox-layer-portals-p (plist-get candidate-state :root-node)))
|
||||||
|
(list (ebox--patch-op
|
||||||
|
'owner-rerender
|
||||||
|
(plist-get (plist-get candidate-state :root-node) :node-id)
|
||||||
|
:dirty (ebox--merge-dirty-provenance-list dirty-set)
|
||||||
|
:layer-recompose-p t))
|
||||||
|
(ebox-patch-plan-merge-ops parents (nreverse operations)))))))
|
||||||
|
|
||||||
|
(defun ebox-incremental--layer-span-proof
|
||||||
|
(buffer old-state prepared candidate-state owner-plan path-copied-p)
|
||||||
|
"Prove the stable outer allocation of one recomposed layer owner.
|
||||||
|
Compare BUFFER's OLD-STATE with CANDIDATE-STATE using PREPARED dirty facts
|
||||||
|
and OWNER-PLAN. PATH-COPIED-P requires private nodes inside that owner.
|
||||||
|
Internal visibility and role topology may change. The surface validates the
|
||||||
|
complete replacement footprint before publishing any resulting spans."
|
||||||
|
(when (and (= (length owner-plan) 1)
|
||||||
|
(plist-get (car owner-plan) :layer-recompose-p))
|
||||||
|
(let* ((owner-id (plist-get (car owner-plan) :owner-id))
|
||||||
|
(node (ebox-runtime-index-get
|
||||||
|
owner-id (plist-get candidate-state :node-table)))
|
||||||
|
(snapshot (ebox-incremental--cached-layout-snapshot-details
|
||||||
|
buffer owner-id))
|
||||||
|
(spans (plist-get snapshot :buffer-spans))
|
||||||
|
(ancestors (ebox-incremental--ancestor-slot-signature
|
||||||
|
buffer old-state candidate-state owner-id)))
|
||||||
|
(when (and node snapshot spans ancestors
|
||||||
|
(ebox-incremental--same-root-surface-identity-p
|
||||||
|
old-state candidate-state)
|
||||||
|
(ebox-incremental--span-patch-identity-stable-p
|
||||||
|
buffer old-state candidate-state prepared owner-id)
|
||||||
|
(or (not path-copied-p)
|
||||||
|
(not (ebox-incremental--candidate-shares-published-node-p
|
||||||
|
old-state node)))
|
||||||
|
(not (ebox--node-visible-overflow-p node))
|
||||||
|
(not (ebox--ancestor-own-overflow-visible-p buffer owner-id))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(or (ebox-buffer--partial-line-slots-p spans)
|
||||||
|
(ebox--spans-contiguous-lines-p spans))))
|
||||||
|
(list :owner-id owner-id :snapshot snapshot
|
||||||
|
:dirty-set (plist-get prepared :dirty-set)
|
||||||
|
:layer-recompose-p t :ancestor-slot-signature ancestors
|
||||||
|
:layout-snapshot-detail-generation
|
||||||
|
(plist-get old-state :layout-snapshot-detail-generation))))))
|
||||||
|
|
||||||
(defun ebox-incremental--cascade-root-owner-plan-p
|
(defun ebox-incremental--cascade-root-owner-plan-p
|
||||||
(old-state candidate-state dirty-set)
|
(old-state candidate-state dirty-set)
|
||||||
"Return non-nil when cascade makes a local proof unusable.
|
"Return non-nil when cascade makes a local proof unusable.
|
||||||
@ -10066,7 +10171,10 @@ role, and outside-complement compatibility before publication."
|
|||||||
"Return one atomic single- or multi-owner projection proof, or nil."
|
"Return one atomic single- or multi-owner projection proof, or nil."
|
||||||
(if (= (length owner-plan) 1)
|
(if (= (length owner-plan) 1)
|
||||||
(when-let* ((proof
|
(when-let* ((proof
|
||||||
(or (ebox-incremental--single-span-patch-projection-proof
|
(or (ebox-incremental--layer-span-proof
|
||||||
|
buffer old-state prepared candidate-state owner-plan
|
||||||
|
path-copied-p)
|
||||||
|
(ebox-incremental--single-span-patch-projection-proof
|
||||||
buffer old-state prepared candidate-state owner-plan
|
buffer old-state prepared candidate-state owner-plan
|
||||||
path-copied-p)
|
path-copied-p)
|
||||||
(ebox-incremental--grouped-owner-span-proof
|
(ebox-incremental--grouped-owner-span-proof
|
||||||
@ -10135,7 +10243,8 @@ role, and outside-complement compatibility before publication."
|
|||||||
;; new line back into that retained slot before TP sees it;
|
;; new line back into that retained slot before TP sees it;
|
||||||
;; raw string length is not the geometry proof.
|
;; raw string length is not the geometry proof.
|
||||||
(cl-every (lambda (proof)
|
(cl-every (lambda (proof)
|
||||||
(or (plist-get proof :variable-content-p)
|
(or (plist-get proof :layer-recompose-p)
|
||||||
|
(plist-get proof :variable-content-p)
|
||||||
(let* ((dirty (car (plist-get proof :dirty-set)))
|
(let* ((dirty (car (plist-get proof :dirty-set)))
|
||||||
(old-content
|
(old-content
|
||||||
(plist-get
|
(plist-get
|
||||||
@ -10239,6 +10348,11 @@ role, and outside-complement compatibility before publication."
|
|||||||
(append
|
(append
|
||||||
(list :viewport-width (plist-get old-state :viewport-width)
|
(list :viewport-width (plist-get old-state :viewport-width)
|
||||||
:viewport-height (plist-get old-state :viewport-height)
|
:viewport-height (plist-get old-state :viewport-height)
|
||||||
|
:layer-recompose-p
|
||||||
|
(and (fboundp 'ebox-layer-host-p)
|
||||||
|
(cl-some (lambda (proof)
|
||||||
|
(plist-get proof :layer-recompose-p))
|
||||||
|
(ebox-incremental--owner-proof-list span-proof)))
|
||||||
:runtime-revision
|
:runtime-revision
|
||||||
(plist-get candidate-state :runtime-revision)
|
(plist-get candidate-state :runtime-revision)
|
||||||
:display-signature (plist-get prepared :display-signature)
|
:display-signature (plist-get prepared :display-signature)
|
||||||
@ -10486,8 +10600,11 @@ role, and outside-complement compatibility before publication."
|
|||||||
'structure))
|
'structure))
|
||||||
(plist-get entry :range-delta)))
|
(plist-get entry :range-delta)))
|
||||||
(plist-get prepared :dirty-set))))))
|
(plist-get prepared :dirty-set))))))
|
||||||
|
(layer-owner-plan
|
||||||
|
(ebox-incremental--layer-owner-plan
|
||||||
|
buffer old-state candidate-state (plist-get prepared :dirty-set)))
|
||||||
(formatting-proof
|
(formatting-proof
|
||||||
(and (not native-program-p)
|
(and (not layer-owner-plan) (not native-program-p)
|
||||||
(not (cl-some
|
(not (cl-some
|
||||||
(lambda (entry)
|
(lambda (entry)
|
||||||
(eq (plist-get entry :dirty-kind) 'paint))
|
(eq (plist-get entry :dirty-kind) 'paint))
|
||||||
@ -10495,7 +10612,7 @@ role, and outside-complement compatibility before publication."
|
|||||||
(ebox-incremental--formatting-context-reflow-proof
|
(ebox-incremental--formatting-context-reflow-proof
|
||||||
buffer old-state candidate-state prepared)))
|
buffer old-state candidate-state prepared)))
|
||||||
(mixed-shape-p
|
(mixed-shape-p
|
||||||
(and (not native-program-p)
|
(and (not layer-owner-plan) (not native-program-p)
|
||||||
(or (not structural-dirty-p) range-structural-p)
|
(or (not structural-dirty-p) range-structural-p)
|
||||||
(cl-some
|
(cl-some
|
||||||
(lambda (entry)
|
(lambda (entry)
|
||||||
@ -10532,6 +10649,7 @@ role, and outside-complement compatibility before publication."
|
|||||||
(plist-get mixed-proof :paint-owner-ids)))))
|
(plist-get mixed-proof :paint-owner-ids)))))
|
||||||
(owner-plan
|
(owner-plan
|
||||||
(cond
|
(cond
|
||||||
|
(layer-owner-plan layer-owner-plan)
|
||||||
((/= (ebox-tree-author-style-count
|
((/= (ebox-tree-author-style-count
|
||||||
(plist-get old-state :root-node))
|
(plist-get old-state :root-node))
|
||||||
(ebox-tree-author-style-count
|
(ebox-tree-author-style-count
|
||||||
@ -10577,7 +10695,9 @@ role, and outside-complement compatibility before publication."
|
|||||||
(ebox-incremental--paint-projection-p
|
(ebox-incremental--paint-projection-p
|
||||||
buffer old-state prepared candidate-state owner-plan)))
|
buffer old-state prepared candidate-state owner-plan)))
|
||||||
(span-proof
|
(span-proof
|
||||||
(unless (or native-program-p structural-dirty-p paint-p mixed-proof)
|
(unless (or native-program-p
|
||||||
|
(and structural-dirty-p (not layer-owner-plan))
|
||||||
|
paint-p mixed-proof)
|
||||||
(ebox-incremental--span-patch-projection-proof
|
(ebox-incremental--span-patch-projection-proof
|
||||||
buffer old-state prepared candidate-state owner-plan)))
|
buffer old-state prepared candidate-state owner-plan)))
|
||||||
(projection-kind
|
(projection-kind
|
||||||
@ -10593,6 +10713,8 @@ role, and outside-complement compatibility before publication."
|
|||||||
(span-proof 'span-patch))))
|
(span-proof 'span-patch))))
|
||||||
(when projection-kind
|
(when projection-kind
|
||||||
(setq report (plist-put report :projection-kind projection-kind)))
|
(setq report (plist-put report :projection-kind projection-kind)))
|
||||||
|
(when layer-owner-plan
|
||||||
|
(setq report (plist-put report :layer-recompose-p t)))
|
||||||
(when mixed-proof
|
(when mixed-proof
|
||||||
(setq prepared (plist-put prepared :mixed-owner-proof mixed-proof))
|
(setq prepared (plist-put prepared :mixed-owner-proof mixed-proof))
|
||||||
(setq report
|
(setq report
|
||||||
@ -10722,8 +10844,11 @@ the visible replacement independently before publishing either one."
|
|||||||
|
|
||||||
(defun ebox-incremental--prepare-scoped-candidate
|
(defun ebox-incremental--prepare-scoped-candidate
|
||||||
(buffer old-state next-root isolated-candidate-p
|
(buffer old-state next-root isolated-candidate-p
|
||||||
&optional skip-span-proof-p scroll-transaction-p)
|
&optional skip-span-proof-p scroll-transaction-p scroll-owner-id)
|
||||||
"Prepare NEXT-ROOT and its scoped owner plan for BUFFER.
|
"Prepare NEXT-ROOT and its scoped owner plan for BUFFER.
|
||||||
|
OLD-STATE provides the published inputs. ISOLATED-CANDIDATE-P marks a private
|
||||||
|
logical tree. SKIP-SPAN-PROOF-P permits the retained scroll fast path;
|
||||||
|
SCROLL-TRANSACTION-P and SCROLL-OWNER-ID identify the changing scroll owner.
|
||||||
Return the intermediate candidate state, owner plan, projection proof, and
|
Return the intermediate candidate state, owner plan, projection proof, and
|
||||||
whether NEXT-ROOT shares untouched published nodes."
|
whether NEXT-ROOT shares untouched published nodes."
|
||||||
(let* ((ebox-incremental--allocated-slot-proof-cache
|
(let* ((ebox-incremental--allocated-slot-proof-cache
|
||||||
@ -10783,8 +10908,16 @@ whether NEXT-ROOT shares untouched published nodes."
|
|||||||
(ebox-source--index-derived-stale-p
|
(ebox-source--index-derived-stale-p
|
||||||
(plist-get candidate-state :source-index)))
|
(plist-get candidate-state :source-index)))
|
||||||
(plist-put candidate-state :selector-index-stale-p t)))
|
(plist-put candidate-state :selector-index-stale-p t)))
|
||||||
|
(layer-owner-plan
|
||||||
|
(ebox-incremental--layer-owner-plan
|
||||||
|
buffer old-state candidate-state
|
||||||
|
(or (plist-get prepared :dirty-set)
|
||||||
|
(and scroll-transaction-p scroll-owner-id
|
||||||
|
(list (list :node-id scroll-owner-id
|
||||||
|
:dirty-kind 'geometry
|
||||||
|
:changed-keys '(:scroll-offset)))))))
|
||||||
(formatting-proof
|
(formatting-proof
|
||||||
(and (not scroll-transaction-p)
|
(and (not layer-owner-plan) (not scroll-transaction-p)
|
||||||
(not (cl-some
|
(not (cl-some
|
||||||
(lambda (entry)
|
(lambda (entry)
|
||||||
(eq (plist-get entry :dirty-kind) 'paint))
|
(eq (plist-get entry :dirty-kind) 'paint))
|
||||||
@ -10792,7 +10925,8 @@ whether NEXT-ROOT shares untouched published nodes."
|
|||||||
(ebox-incremental--formatting-context-reflow-proof
|
(ebox-incremental--formatting-context-reflow-proof
|
||||||
buffer old-state candidate-state prepared)))
|
buffer old-state candidate-state prepared)))
|
||||||
(owner-plan
|
(owner-plan
|
||||||
(if formatting-proof
|
(if layer-owner-plan layer-owner-plan
|
||||||
|
(if formatting-proof
|
||||||
(list
|
(list
|
||||||
(ebox--patch-op
|
(ebox--patch-op
|
||||||
'formatting-context-reflow
|
'formatting-context-reflow
|
||||||
@ -10804,9 +10938,9 @@ whether NEXT-ROOT shares untouched published nodes."
|
|||||||
candidate-state))
|
candidate-state))
|
||||||
(ebox-incremental--layout-owner-plan
|
(ebox-incremental--layout-owner-plan
|
||||||
buffer old-state candidate-state
|
buffer old-state candidate-state
|
||||||
(plist-get prepared :dirty-set))))))
|
(plist-get prepared :dirty-set)))))))
|
||||||
(mixed-proof
|
(mixed-proof
|
||||||
(and (not scroll-transaction-p)
|
(and (not layer-owner-plan) (not scroll-transaction-p)
|
||||||
(ebox-incremental--mixed-owner-proof
|
(ebox-incremental--mixed-owner-proof
|
||||||
buffer old-state candidate-state prepared owner-plan)))
|
buffer old-state candidate-state prepared owner-plan)))
|
||||||
(scope-node-ids
|
(scope-node-ids
|
||||||
@ -10818,7 +10952,8 @@ whether NEXT-ROOT shares untouched published nodes."
|
|||||||
(ebox-incremental--paint-projection-p
|
(ebox-incremental--paint-projection-p
|
||||||
buffer old-state prepared candidate-state owner-plan))
|
buffer old-state prepared candidate-state owner-plan))
|
||||||
(span-proof
|
(span-proof
|
||||||
(unless (or paint-p mixed-proof skip-span-proof-p)
|
(unless (or paint-p mixed-proof
|
||||||
|
(and skip-span-proof-p (not layer-owner-plan)))
|
||||||
(or (ebox-incremental--span-patch-projection-proof
|
(or (ebox-incremental--span-patch-projection-proof
|
||||||
buffer old-state prepared candidate-state owner-plan
|
buffer old-state prepared candidate-state owner-plan
|
||||||
path-copied-p)
|
path-copied-p)
|
||||||
@ -10839,7 +10974,7 @@ whether NEXT-ROOT shares untouched published nodes."
|
|||||||
scope-node-ids (mapcar (lambda (op) (plist-get op :owner-id))
|
scope-node-ids (mapcar (lambda (op) (plist-get op :owner-id))
|
||||||
owner-plan)))
|
owner-plan)))
|
||||||
(setq prepared (plist-put prepared :scroll-patch-fast-p
|
(setq prepared (plist-put prepared :scroll-patch-fast-p
|
||||||
skip-span-proof-p))
|
(and skip-span-proof-p (not layer-owner-plan))))
|
||||||
(when mixed-proof
|
(when mixed-proof
|
||||||
(setq prepared (plist-put prepared :mixed-owner-proof mixed-proof)))
|
(setq prepared (plist-put prepared :mixed-owner-proof mixed-proof)))
|
||||||
(list :prepared prepared
|
(list :prepared prepared
|
||||||
@ -10849,6 +10984,7 @@ whether NEXT-ROOT shares untouched published nodes."
|
|||||||
:span-proof span-proof
|
:span-proof span-proof
|
||||||
:mixed-proof mixed-proof
|
:mixed-proof mixed-proof
|
||||||
:formatting-proof formatting-proof
|
:formatting-proof formatting-proof
|
||||||
|
:layer-recompose-p (and layer-owner-plan t)
|
||||||
:projection-kind projection-kind
|
:projection-kind projection-kind
|
||||||
:path-copied-p path-copied-p)))
|
:path-copied-p path-copied-p)))
|
||||||
|
|
||||||
@ -10870,13 +11006,15 @@ runtime candidate whose identities already match the published root."
|
|||||||
(let* ((candidate
|
(let* ((candidate
|
||||||
(ebox-incremental--prepare-scoped-candidate
|
(ebox-incremental--prepare-scoped-candidate
|
||||||
buffer old-state next-root isolated-candidate-p
|
buffer old-state next-root isolated-candidate-p
|
||||||
scroll-patch-fast-p scroll-transaction-p)))
|
scroll-patch-fast-p scroll-transaction-p
|
||||||
|
(plist-get report-overrides :owner-id))))
|
||||||
;; A path-copied candidate is safe to render in place only after the
|
;; A path-copied candidate is safe to render in place only after the
|
||||||
;; strict span proof succeeds. Widened projection would mutate shared
|
;; strict span proof succeeds. Widened projection would mutate shared
|
||||||
;; layout nodes, so promote it to a private copy before the surface sees
|
;; layout nodes, so promote it to a private copy before the surface sees
|
||||||
;; it. The ordinary full-copy path remains the correctness fallback.
|
;; it. The ordinary full-copy path remains the correctness fallback.
|
||||||
(when (and (plist-get candidate :path-copied-p)
|
(when (and (plist-get candidate :path-copied-p)
|
||||||
(not scroll-patch-fast-p)
|
(or (not scroll-patch-fast-p)
|
||||||
|
(plist-get candidate :layer-recompose-p))
|
||||||
(not (memq (plist-get candidate :projection-kind)
|
(not (memq (plist-get candidate :projection-kind)
|
||||||
'(span-patch owner-scoped scroll-patch
|
'(span-patch owner-scoped scroll-patch
|
||||||
mixed-owner-reflow))))
|
mixed-owner-reflow))))
|
||||||
@ -10886,8 +11024,11 @@ runtime candidate whose identities already match the published root."
|
|||||||
(ebox-tree-copy-node-structure
|
(ebox-tree-copy-node-structure
|
||||||
(plist-get (plist-get candidate :prepared) :root))
|
(plist-get (plist-get candidate :prepared) :root))
|
||||||
isolated-candidate-p scroll-patch-fast-p
|
isolated-candidate-p scroll-patch-fast-p
|
||||||
scroll-transaction-p)))
|
scroll-transaction-p (plist-get report-overrides :owner-id))))
|
||||||
(let* ((prepared (plist-get candidate :prepared))
|
(let* ((prepared (plist-get candidate :prepared))
|
||||||
|
(scroll-transaction-p
|
||||||
|
(and scroll-transaction-p
|
||||||
|
(not (plist-get candidate :layer-recompose-p))))
|
||||||
(candidate-state (plist-get candidate :candidate-state))
|
(candidate-state (plist-get candidate :candidate-state))
|
||||||
(owner-plan
|
(owner-plan
|
||||||
(or (plist-get candidate :owner-plan)
|
(or (plist-get candidate :owner-plan)
|
||||||
@ -10928,6 +11069,8 @@ runtime candidate whose identities already match the published root."
|
|||||||
(setq report (plist-put report :projection-kind projection-kind)))
|
(setq report (plist-put report :projection-kind projection-kind)))
|
||||||
(when projection-kind
|
(when projection-kind
|
||||||
(setq report (plist-put report :projection-kind projection-kind)))
|
(setq report (plist-put report :projection-kind projection-kind)))
|
||||||
|
(when (plist-get candidate :layer-recompose-p)
|
||||||
|
(setq report (plist-put report :layer-recompose-p t)))
|
||||||
(when span-proof
|
(when span-proof
|
||||||
(setq report
|
(setq report
|
||||||
(plist-put report :ancestor-slot-count
|
(plist-put report :ancestor-slot-count
|
||||||
|
|||||||
358
ebox-layer.el
Normal file
358
ebox-layer.el
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
;;; ebox-layer.el --- Retained box placement and text composition -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Compose visible pieces without changing the logical tree. Horizontal
|
||||||
|
;; coordinates are pixels; vertical coordinates share the host's text rows.
|
||||||
|
;; This module never writes a buffer or owns application state.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ebox-tree)
|
||||||
|
(require 'ebox-composite)
|
||||||
|
|
||||||
|
(declare-function ebox-get "ebox" (box property))
|
||||||
|
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||||
|
(declare-function ebox--substring-pixel-width "ebox-measure" (string start end))
|
||||||
|
(declare-function ebox--string-pixel-width "ebox-measure" (string))
|
||||||
|
(declare-function ebox-string-lines "ebox" (string))
|
||||||
|
(declare-function ebox-lines-join "ebox" (lines))
|
||||||
|
(declare-function ebox--render-with-cache "ebox" (node &optional width probe))
|
||||||
|
(declare-function ebox--call-with-box-content-context "ebox-layout" (box function))
|
||||||
|
(declare-function ebox--wrapper-content-viewport-pixel "ebox-layout" (box))
|
||||||
|
(declare-function ebox--content-height "ebox-layout" (box height &optional rendered))
|
||||||
|
(declare-function ebox--size-pixels "ebox-layout" (value &optional box axis percent-base))
|
||||||
|
(declare-function ebox--size-line-height "ebox-layout" (&optional box))
|
||||||
|
(declare-function ebox--record-rendered-uniform-width "ebox-layout" (rendered width))
|
||||||
|
(declare-function ebox--record-rendered-intrinsic-size "ebox-layout" (rendered width height))
|
||||||
|
(declare-function ebox-pixel-space "ebox" (pixels))
|
||||||
|
(defvar ebox-region-types)
|
||||||
|
(defvar ebox--intrinsic-layout-measurement)
|
||||||
|
(defvar ebox--render-source-index)
|
||||||
|
|
||||||
|
(defvar ebox-layer--render-root nil
|
||||||
|
"Root of this spatial materialization, never a live buffer root.")
|
||||||
|
(defvar ebox-layer--portals nil
|
||||||
|
"Candidate-local absolute nodes awaiting root composition.")
|
||||||
|
|
||||||
|
(defvar ebox-layer--active-p 'unknown
|
||||||
|
"Whether the current candidate contains layered layout.
|
||||||
|
Retained publication supplies a known boolean; isolated rendering resolves
|
||||||
|
the fact once for its spatial root.")
|
||||||
|
|
||||||
|
(defun ebox-layer-active-p (&optional node)
|
||||||
|
"Return the current candidate's layer capability without repeated scans."
|
||||||
|
(if (eq ebox-layer--active-p 'unknown)
|
||||||
|
(if ebox-layer--render-root
|
||||||
|
(setq ebox-layer--active-p
|
||||||
|
(ebox-layer-subtree-p ebox-layer--render-root))
|
||||||
|
(ebox-layer-subtree-p node))
|
||||||
|
ebox-layer--active-p))
|
||||||
|
|
||||||
|
(defun ebox-layer-positioned-p (node)
|
||||||
|
"Return non-nil when NODE opts into positioned box painting."
|
||||||
|
(memq (plist-get node :position) '(relative absolute)))
|
||||||
|
|
||||||
|
(defun ebox-layer-host-p (node)
|
||||||
|
"Return non-nil when NODE composes positioned direct children."
|
||||||
|
(and (eq (plist-get node :ebox-kind) 'box)
|
||||||
|
(cl-some #'ebox-layer-positioned-p
|
||||||
|
(ebox-tree-layout-children node))))
|
||||||
|
|
||||||
|
(defun ebox-layer-subtree-p (node)
|
||||||
|
"Return non-nil when NODE's logical subtree contains positioned painting."
|
||||||
|
(and (listp node)
|
||||||
|
(or (ebox-layer-host-p node)
|
||||||
|
(ebox-layer-positioned-p node)
|
||||||
|
(cl-some #'ebox-layer-subtree-p (ebox-tree-node-children node)))))
|
||||||
|
|
||||||
|
(defun ebox-layer-portals-p (node)
|
||||||
|
"Return non-nil when NODE's subtree projects content to its render root."
|
||||||
|
(and (listp node)
|
||||||
|
(or (eq (plist-get node :layer) 'root)
|
||||||
|
(cl-some #'ebox-layer-portals-p (ebox-tree-node-children node)))))
|
||||||
|
|
||||||
|
(defun ebox-layer--owned-p (line position region)
|
||||||
|
"Whether LINE at POSITION visibly belongs to REGION, excluding margins."
|
||||||
|
(and (not (cl-some (lambda (role)
|
||||||
|
(equal region (get-text-property position role line)))
|
||||||
|
'(ebox-ml ebox-mr ebox-mt ebox-mb)))
|
||||||
|
(or (member region (get-text-property position 'ebox-content-owners line))
|
||||||
|
(equal region (get-text-property position 'ebox-content-owner line))
|
||||||
|
(cl-some (lambda (role)
|
||||||
|
(equal region (get-text-property position (cdr role) line)))
|
||||||
|
ebox-region-types))))
|
||||||
|
|
||||||
|
(defun ebox-layer--pieces (lines node)
|
||||||
|
"Return NODE's visible source intervals in LINES, before positioning."
|
||||||
|
(let ((region (ebox--ensure-region-id node)) pieces)
|
||||||
|
(cl-loop for line in lines for row from 0 do
|
||||||
|
(let ((start 0) (limit (length line)))
|
||||||
|
(while (< start limit)
|
||||||
|
(let ((end (next-property-change start line limit)))
|
||||||
|
(when (ebox-layer--owned-p line start region)
|
||||||
|
(let ((x (ebox--substring-pixel-width line 0 start))
|
||||||
|
(right (ebox--substring-pixel-width line 0 end)))
|
||||||
|
(when (> right x)
|
||||||
|
;; Keep the full row as source so shaping and shared
|
||||||
|
;; display objects are measured in their own context.
|
||||||
|
(push (list :line line :start x :end right :y row)
|
||||||
|
pieces))))
|
||||||
|
(setq start end)))))
|
||||||
|
;; Property boundaries must not become artificial glyph clipping edges.
|
||||||
|
(let (merged)
|
||||||
|
(dolist (piece (nreverse pieces))
|
||||||
|
(let ((last (car merged)))
|
||||||
|
(if (and last (= (plist-get last :y) (plist-get piece :y))
|
||||||
|
(= (plist-get last :end) (plist-get piece :start)))
|
||||||
|
(plist-put last :end (plist-get piece :end))
|
||||||
|
(push piece merged))))
|
||||||
|
(nreverse merged))))
|
||||||
|
|
||||||
|
(defun ebox-layer--bounds (pieces)
|
||||||
|
"Return (X Y WIDTH HEIGHT) covering nonempty PIECES, or nil."
|
||||||
|
(when pieces
|
||||||
|
(let ((left (apply #'min (mapcar (lambda (p) (plist-get p :start)) pieces)))
|
||||||
|
(right (apply #'max (mapcar (lambda (p) (plist-get p :end)) pieces)))
|
||||||
|
(top (apply #'min (mapcar (lambda (p) (plist-get p :y)) pieces)))
|
||||||
|
(bottom (apply #'max (mapcar (lambda (p) (plist-get p :y)) pieces))))
|
||||||
|
(list left top (- right left) (1+ (- bottom top))))))
|
||||||
|
|
||||||
|
(defun ebox-layer--offset (node property host extent)
|
||||||
|
"Resolve NODE's PROPERTY against HOST and containing axis EXTENT."
|
||||||
|
(let* ((vertical (eq property :top))
|
||||||
|
(value (or (plist-get node property) (if vertical '(lh 0) '(px 0))))
|
||||||
|
(pixels (ebox--size-pixels value host (if vertical 'height 'width)
|
||||||
|
(if vertical
|
||||||
|
(* extent (ebox--size-line-height host))
|
||||||
|
extent))))
|
||||||
|
(unless (numberp pixels)
|
||||||
|
(error "Ebox layer offset has no definite containing size: %S" value))
|
||||||
|
(if vertical (floor (/ pixels (float (ebox--size-line-height host)))) pixels)))
|
||||||
|
|
||||||
|
(defun ebox-layer--validate-child (node host)
|
||||||
|
"Check text backend placement contracts for NODE inside HOST."
|
||||||
|
(ebox-layer-validate-node node)
|
||||||
|
(when (ebox-layer-positioned-p node)
|
||||||
|
(cl-labels ((check (child)
|
||||||
|
(unless (= (ebox--size-line-height child)
|
||||||
|
(ebox--size-line-height host))
|
||||||
|
(error "Ebox positioned boxes must share their host's line height"))
|
||||||
|
(mapc #'check (ebox-tree-node-children child))))
|
||||||
|
(check node))))
|
||||||
|
|
||||||
|
(defun ebox-layer-validate-node (node)
|
||||||
|
"Validate NODE's placement combinations independently of visibility."
|
||||||
|
(when (and (or (plist-get node :anchor)
|
||||||
|
(eq (plist-get node :layer) 'root))
|
||||||
|
(not (eq (plist-get node :position) 'absolute)))
|
||||||
|
(error "Ebox :anchor and :layer root require :position absolute")))
|
||||||
|
|
||||||
|
(defun ebox-layer--ordered-records (host records)
|
||||||
|
"Sort candidate RECORDS by layer and HOST logical document order."
|
||||||
|
(let ((order (make-hash-table :test #'eq)) (index 0))
|
||||||
|
(cl-labels ((visit (node)
|
||||||
|
(puthash node (cl-incf index) order)
|
||||||
|
(mapc #'visit (ebox-tree-node-children node))))
|
||||||
|
(visit host))
|
||||||
|
(cl-stable-sort
|
||||||
|
(copy-sequence records)
|
||||||
|
(lambda (a b)
|
||||||
|
(let* ((an (plist-get a :node)) (bn (plist-get b :node))
|
||||||
|
(az (or (plist-get an :z-index) 0))
|
||||||
|
(bz (or (plist-get bn :z-index) 0)))
|
||||||
|
(cond
|
||||||
|
((/= az bz) (< az bz))
|
||||||
|
((not (eq (and (ebox-layer-positioned-p an) t)
|
||||||
|
(and (ebox-layer-positioned-p bn) t)))
|
||||||
|
(not (ebox-layer-positioned-p an)))
|
||||||
|
(t (< (gethash an order 0) (gethash bn order 0)))))))))
|
||||||
|
|
||||||
|
(defun ebox-layer--compose-rows (host records width height)
|
||||||
|
"Compose HOST's visible RECORDS in a WIDTH by HEIGHT content canvas."
|
||||||
|
(let ((rows (make-vector height nil)))
|
||||||
|
(dolist (record (ebox-layer--ordered-records host records))
|
||||||
|
(let ((row (plist-get record :y)))
|
||||||
|
(when (and (>= row 0) (< row height))
|
||||||
|
(push record (aref rows row)))))
|
||||||
|
(cl-loop for row across rows collect
|
||||||
|
(ebox-composite-line "" (nreverse row) width))))
|
||||||
|
|
||||||
|
(defun ebox-layer--anchor-node (host id)
|
||||||
|
"Find one logical anchor ID under HOST, rejecting ambiguous IDs."
|
||||||
|
(let ((name (format "%s" id)) matches)
|
||||||
|
(cl-labels ((visit (node)
|
||||||
|
(when (equal name (ebox-tree-node-id ebox--render-source-index node))
|
||||||
|
(push node matches))
|
||||||
|
(mapc #'visit (ebox-tree-node-children node))))
|
||||||
|
(visit host))
|
||||||
|
(when (cdr matches) (error "Ebox layer anchor is ambiguous: %s" name))
|
||||||
|
(car matches)))
|
||||||
|
|
||||||
|
(defun ebox-layer--absolute-origin (node host bounds width height layer-width layer-height)
|
||||||
|
"Resolve NODE's origin in HOST's BASE, or nil for an invisible anchor.
|
||||||
|
BOUNDS is the already resolved anchor rectangle, or nil.
|
||||||
|
WIDTH and HEIGHT bound the host; LAYER-WIDTH and LAYER-HEIGHT bound the panel."
|
||||||
|
(let ((x (ebox-layer--offset node :left host width))
|
||||||
|
(y (ebox-layer--offset node :top host height))
|
||||||
|
(anchor (plist-get node :anchor)))
|
||||||
|
(if (not anchor) (list x y)
|
||||||
|
(when bounds
|
||||||
|
(let* ((placement (or (plist-get node :placement) 'bottom-start))
|
||||||
|
(above (memq placement '(top-start top-end)))
|
||||||
|
(left (+ (car bounds) x
|
||||||
|
(if (memq placement '(bottom-end top-end))
|
||||||
|
(- (nth 2 bounds) layer-width) 0)))
|
||||||
|
(top (+ y (nth 1 bounds)
|
||||||
|
(if above (- layer-height) (nth 3 bounds))))
|
||||||
|
(other (+ y (nth 1 bounds)
|
||||||
|
(if above (nth 3 bounds) (- layer-height)))))
|
||||||
|
(when (and (or (< top 0) (> (+ top layer-height) height))
|
||||||
|
(>= other 0) (<= (+ other layer-height) height))
|
||||||
|
(setq top other))
|
||||||
|
(list (max 0 (min left (max 0 (- width layer-width))))
|
||||||
|
(max 0 (min top (max 0 (- height layer-height))))))))))
|
||||||
|
|
||||||
|
(defun ebox-layer--visible-bounds (bounds origin width height)
|
||||||
|
"Translate BOUNDS by ORIGIN and intersect the WIDTH by HEIGHT canvas."
|
||||||
|
(when (and bounds origin)
|
||||||
|
(let* ((x (+ (car bounds) (car origin)))
|
||||||
|
(y (+ (cadr bounds) (cadr origin)))
|
||||||
|
(left (max 0 x)) (top (max 0 y))
|
||||||
|
(right (min width (+ x (nth 2 bounds))))
|
||||||
|
(bottom (min height (+ y (nth 3 bounds)))))
|
||||||
|
(when (and (> right left) (> bottom top))
|
||||||
|
(list left top (- right left) (- bottom top))))))
|
||||||
|
|
||||||
|
(defun ebox-layer--absolute-records (host nodes base width height)
|
||||||
|
"Resolve HOST's absolute NODES and return opaque paint records.
|
||||||
|
BASE supplies normal-flow anchor fragments. Anchor dependencies are resolved
|
||||||
|
before painting, so source order and z order never select the geometry.
|
||||||
|
Missing or clipped anchors suppress their dependents; cycles signal before
|
||||||
|
publication. Nested root projections join this same candidate-local pass."
|
||||||
|
(let ((panels (make-hash-table :test #'eq))
|
||||||
|
(parents (make-hash-table :test #'eq))
|
||||||
|
(status (make-hash-table :test #'eq))
|
||||||
|
(queue (copy-sequence nodes)) order records)
|
||||||
|
(cl-labels
|
||||||
|
((index (node)
|
||||||
|
(dolist (child (ebox-tree-node-children node))
|
||||||
|
(puthash child node parents)
|
||||||
|
(index child)))
|
||||||
|
(panel-owner (node)
|
||||||
|
(while (and node (not (gethash node panels)))
|
||||||
|
(setq node (gethash node parents)))
|
||||||
|
node)
|
||||||
|
(resolve (node)
|
||||||
|
(pcase (gethash node status)
|
||||||
|
('visiting (error "Ebox layer anchor dependency is cyclic"))
|
||||||
|
('done (plist-get (gethash node panels) :origin))
|
||||||
|
(_
|
||||||
|
(puthash node 'visiting status)
|
||||||
|
(let* ((panel (gethash node panels))
|
||||||
|
(anchor (plist-get node :anchor))
|
||||||
|
(target (and anchor (ebox-layer--anchor-node host anchor)))
|
||||||
|
(provider (and target (panel-owner target)))
|
||||||
|
(bounds
|
||||||
|
(when (and target (not (eq (plist-get target :visibility) 'hidden)))
|
||||||
|
(if provider
|
||||||
|
(let ((origin (resolve provider)))
|
||||||
|
(ebox-layer--visible-bounds
|
||||||
|
(ebox-layer--bounds
|
||||||
|
(ebox-layer--pieces
|
||||||
|
(plist-get (gethash provider panels) :lines) target))
|
||||||
|
origin width height))
|
||||||
|
(ebox-layer--bounds (ebox-layer--pieces base target)))))
|
||||||
|
(origin
|
||||||
|
(unless (eq (plist-get node :visibility) 'hidden)
|
||||||
|
(ebox-layer--absolute-origin
|
||||||
|
node host bounds width height
|
||||||
|
(plist-get panel :width) (length (plist-get panel :lines))))))
|
||||||
|
(plist-put panel :origin origin)
|
||||||
|
(puthash node 'done status)
|
||||||
|
origin)))))
|
||||||
|
(index host)
|
||||||
|
(while queue
|
||||||
|
(let ((node (pop queue)))
|
||||||
|
(unless (gethash node panels)
|
||||||
|
(ebox-layer--validate-child node host)
|
||||||
|
(let* ((text
|
||||||
|
(unless (eq (plist-get node :visibility) 'hidden)
|
||||||
|
(ebox--call-with-box-content-context
|
||||||
|
host (lambda () (ebox--render-with-cache node)))))
|
||||||
|
(lines (and text (ebox-string-lines text))))
|
||||||
|
(puthash node
|
||||||
|
(list :lines lines :origin nil
|
||||||
|
:width (if lines (apply #'max (mapcar #'ebox--string-pixel-width lines)) 0))
|
||||||
|
panels)
|
||||||
|
(push node order)
|
||||||
|
(when (eq host ebox-layer--render-root)
|
||||||
|
(dolist (portal ebox-layer--portals)
|
||||||
|
(unless (or (gethash portal panels) (memq portal queue))
|
||||||
|
(setq queue (append queue (list portal))))))))))
|
||||||
|
(dolist (node (nreverse order))
|
||||||
|
(let ((origin (resolve node)))
|
||||||
|
(when origin
|
||||||
|
(dolist (piece (ebox-layer--pieces (plist-get (gethash node panels) :lines) node))
|
||||||
|
(push (list :node node
|
||||||
|
:x (+ (car origin) (plist-get piece :start))
|
||||||
|
:y (+ (cadr origin) (plist-get piece :y))
|
||||||
|
:text (ebox-composite-slice (plist-get piece :line)
|
||||||
|
(plist-get piece :start)
|
||||||
|
(plist-get piece :end))) records))))))
|
||||||
|
records))
|
||||||
|
|
||||||
|
(defun ebox-layer-render-children (host children renderer)
|
||||||
|
"Compose HOST's CHILDREN using ordinary flow RENDERER.
|
||||||
|
RENDERER accepts a child list. Logical nodes are never reordered or removed.
|
||||||
|
Only candidate-local lists of visible paint records are sorted."
|
||||||
|
(let* ((flow (cl-remove-if (lambda (child)
|
||||||
|
(eq (plist-get child :position) 'absolute)) children))
|
||||||
|
(base (funcall renderer flow)))
|
||||||
|
(if ebox--intrinsic-layout-measurement base
|
||||||
|
(let* ((lines (ebox-string-lines base))
|
||||||
|
(width (or (ebox--wrapper-content-viewport-pixel host)
|
||||||
|
(if lines (apply #'max (mapcar #'ebox--string-pixel-width lines)) 0)))
|
||||||
|
(flow-height (length lines))
|
||||||
|
(height (max flow-height (ceiling (ebox--content-height host flow-height base))))
|
||||||
|
(absolute (cl-remove-if-not (lambda (child)
|
||||||
|
(eq (plist-get child :position) 'absolute)) children))
|
||||||
|
records)
|
||||||
|
(dolist (child children) (ebox-layer--validate-child child host))
|
||||||
|
;; Normal children preserve their layout slots. Relative children use
|
||||||
|
;; the same source intervals and shift only their paint coordinates.
|
||||||
|
(dolist (child flow)
|
||||||
|
(unless (eq (plist-get child :visibility) 'hidden)
|
||||||
|
(let ((dx (if (eq (plist-get child :position) 'relative)
|
||||||
|
(ebox-layer--offset child :left host width) 0))
|
||||||
|
(dy (if (eq (plist-get child :position) 'relative)
|
||||||
|
(ebox-layer--offset child :top host height) 0)))
|
||||||
|
(dolist (piece (ebox-layer--pieces lines child))
|
||||||
|
(push (list :node child :y (+ dy (plist-get piece :y))
|
||||||
|
:x (+ dx (plist-get piece :start))
|
||||||
|
:text (ebox-composite-slice (plist-get piece :line)
|
||||||
|
(plist-get piece :start)
|
||||||
|
(plist-get piece :end))) records)))))
|
||||||
|
(unless (eq host ebox-layer--render-root)
|
||||||
|
(dolist (child absolute)
|
||||||
|
(when (eq (plist-get child :layer) 'root)
|
||||||
|
(cl-pushnew child ebox-layer--portals :test #'eq)))
|
||||||
|
(setq absolute (cl-remove-if (lambda (child)
|
||||||
|
(eq (plist-get child :layer) 'root)) absolute)))
|
||||||
|
(when (eq host ebox-layer--render-root)
|
||||||
|
(setq absolute (append absolute (reverse ebox-layer--portals))))
|
||||||
|
(when (cl-some (lambda (child) (plist-get child :anchor)) absolute)
|
||||||
|
;; Anchor geometry follows relative movement and visible clipping,
|
||||||
|
;; not the original flow slot. This extra composition is needed
|
||||||
|
;; only by anchored panels.
|
||||||
|
(setq lines (ebox-layer--compose-rows host records width height)))
|
||||||
|
(setq records (nconc (ebox-layer--absolute-records host absolute lines width height)
|
||||||
|
records))
|
||||||
|
(ebox--record-rendered-intrinsic-size
|
||||||
|
(ebox--record-rendered-uniform-width
|
||||||
|
(ebox-lines-join (ebox-layer--compose-rows host records width height)) width)
|
||||||
|
width flow-height)))))
|
||||||
|
|
||||||
|
(provide 'ebox-layer)
|
||||||
|
|
||||||
|
;;; ebox-layer.el ends here
|
||||||
@ -17,6 +17,7 @@
|
|||||||
(require 'ebox-size)
|
(require 'ebox-size)
|
||||||
(require 'ebox-buffer-backend)
|
(require 'ebox-buffer-backend)
|
||||||
(require 'ebox-runtime-index)
|
(require 'ebox-runtime-index)
|
||||||
|
(require 'ebox-layer)
|
||||||
|
|
||||||
(declare-function ekp-pixel-justify "ekp" (string line-pixel))
|
(declare-function ekp-pixel-justify "ekp" (string line-pixel))
|
||||||
(declare-function ebox--render-grid "ebox-grid" (node))
|
(declare-function ebox--render-grid "ebox-grid" (node))
|
||||||
@ -1331,7 +1332,9 @@ Return its empty inline extent through `ebox--box-content-empty-width'."
|
|||||||
(ebox--render-box-content-cached
|
(ebox--render-box-content-cached
|
||||||
box
|
box
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(pcase kind
|
(let ((render-flow
|
||||||
|
(lambda (children)
|
||||||
|
(pcase kind
|
||||||
('normal
|
('normal
|
||||||
(ebox--render-normal-box-children box children))
|
(ebox--render-normal-box-children box children))
|
||||||
('row
|
('row
|
||||||
@ -1363,7 +1366,12 @@ Return its empty inline extent through `ebox--box-content-empty-width'."
|
|||||||
(ebox-layout-config-props
|
(ebox-layout-config-props
|
||||||
(plist-get box :ebox-layout-config))
|
(plist-get box :ebox-layout-config))
|
||||||
children))
|
children))
|
||||||
(_ (error "Ebox Box has unsupported LayoutConfig: %S" kind)))))))
|
(_ (error "Ebox Box has unsupported LayoutConfig: %S" kind))))))
|
||||||
|
(if (or (ebox-layer-host-p box)
|
||||||
|
(and (eq box ebox-layer--render-root)
|
||||||
|
(or ebox-layer--portals (ebox-layer-portals-p box))))
|
||||||
|
(ebox-layer-render-children box children render-flow)
|
||||||
|
(funcall render-flow children)))))))
|
||||||
(plist-put box :ebox-content-layout-complete-p
|
(plist-put box :ebox-content-layout-complete-p
|
||||||
(not (and (eq kind 'normal)
|
(not (and (eq kind 'normal)
|
||||||
(eq (ebox-get box :wrap-mode) 'kp))))
|
(eq (ebox-get box :wrap-mode) 'kp))))
|
||||||
@ -1721,7 +1729,8 @@ one carrier line preserves their paint without inventing a content line."
|
|||||||
|
|
||||||
(defun ebox--format-content (box)
|
(defun ebox--format-content (box)
|
||||||
"Format BOX content (wrap and justify) returning a string."
|
"Format BOX content (wrap and justify) returning a string."
|
||||||
(or (ebox--format-scroll-window-content box)
|
(or (and (not (ebox-layer-active-p box))
|
||||||
|
(ebox--format-scroll-window-content box))
|
||||||
(ebox--format-content-string box (ebox--box-content box))))
|
(ebox--format-content-string box (ebox--box-content box))))
|
||||||
|
|
||||||
(defun ebox--total-pixel (box)
|
(defun ebox--total-pixel (box)
|
||||||
@ -2079,6 +2088,15 @@ Internal implementation of `ebox-render' for box nodes."
|
|||||||
rendered))
|
rendered))
|
||||||
|
|
||||||
(defun ebox--render-layout (node)
|
(defun ebox--render-layout (node)
|
||||||
|
"Render NODE in one candidate-local layer collection context."
|
||||||
|
(if ebox-layer--render-root
|
||||||
|
(ebox--render-layout-in-context node)
|
||||||
|
(let ((ebox-layer--render-root node)
|
||||||
|
(ebox-layer--portals nil)
|
||||||
|
(ebox-layer--active-p ebox-layer--active-p))
|
||||||
|
(ebox--render-layout-in-context node))))
|
||||||
|
|
||||||
|
(defun ebox--render-layout-in-context (node)
|
||||||
"Render layout NODE directly to a multi-line propertized string.
|
"Render layout NODE directly to a multi-line propertized string.
|
||||||
NODE can be:
|
NODE can be:
|
||||||
- a box plist created by `ebox-create'
|
- a box plist created by `ebox-create'
|
||||||
@ -2107,6 +2125,7 @@ This function owns spatial layout only; it never creates a live TP surface."
|
|||||||
(or ebox--box-content-render-cache
|
(or ebox--box-content-render-cache
|
||||||
(make-hash-table :test 'eq))))
|
(make-hash-table :test 'eq))))
|
||||||
(when (listp node)
|
(when (listp node)
|
||||||
|
(ebox-layer-validate-node node)
|
||||||
(ebox--ensure-node-id node)
|
(ebox--ensure-node-id node)
|
||||||
(ebox-buffer-validate-border-capability node))
|
(ebox-buffer-validate-border-capability node))
|
||||||
(let ((type (plist-get node :ebox-type)))
|
(let ((type (plist-get node :ebox-type)))
|
||||||
|
|||||||
@ -509,8 +509,10 @@ new session with the same bounded configuration used by retained frames."
|
|||||||
(when (ebox-runtime-index-like-p nodes)
|
(when (ebox-runtime-index-like-p nodes)
|
||||||
(ebox-runtime-index-map
|
(ebox-runtime-index-map
|
||||||
(lambda (_node-id node)
|
(lambda (_node-id node)
|
||||||
(unless (memq (plist-get node :ebox-type)
|
(unless (and (memq (plist-get node :ebox-type)
|
||||||
'(box concat stack flex))
|
'(box concat stack flex))
|
||||||
|
(not (ebox-layer-host-p node))
|
||||||
|
(not (ebox-layer-positioned-p node)))
|
||||||
(setq supported nil)))
|
(setq supported nil)))
|
||||||
nodes))
|
nodes))
|
||||||
supported))
|
supported))
|
||||||
|
|||||||
@ -2428,6 +2428,7 @@ Grid deliberately stays on the ordinary Ebox renderer until the native
|
|||||||
backend has a matching two-dimensional layout contract."
|
backend has a matching two-dimensional layout contract."
|
||||||
(cond
|
(cond
|
||||||
((or (null node) (stringp node) (not (listp node))) t)
|
((or (null node) (stringp node) (not (listp node))) t)
|
||||||
|
((or (ebox-layer-host-p node) (ebox-layer-positioned-p node)) nil)
|
||||||
((eq (plist-get node :visibility) 'hidden) nil)
|
((eq (plist-get node :visibility) 'hidden) nil)
|
||||||
((not (ebox-native-reflow--node-static-sizes-p node)) nil)
|
((not (ebox-native-reflow--node-static-sizes-p node)) nil)
|
||||||
((plist-get node :ebox-child-sequence) nil)
|
((plist-get node :ebox-child-sequence) nil)
|
||||||
|
|||||||
101
ebox-style.el
101
ebox-style.el
@ -70,6 +70,34 @@
|
|||||||
(:name :box-sizing :id ebox/box-sizing :initial border-box
|
(:name :box-sizing :id ebox/box-sizing :initial border-box
|
||||||
:contexts (box row column flex grid) :group geometry
|
:contexts (box row column flex grid) :group geometry
|
||||||
:dirty-kind geometry :signature layout)
|
:dirty-kind geometry :signature layout)
|
||||||
|
(:name :position :id ebox/position :initial static
|
||||||
|
:contexts (box row column flex grid) :group geometry
|
||||||
|
:dirty-kind geometry :signature layout :validator position
|
||||||
|
:runtime-default t)
|
||||||
|
(:name :left :id ebox/left :initial (px 0)
|
||||||
|
:contexts (box row column flex grid) :group geometry
|
||||||
|
:dirty-kind geometry :signature layout
|
||||||
|
:size-axis inline :validator signed-size :runtime-default t)
|
||||||
|
(:name :top :id ebox/top :initial (lh 0)
|
||||||
|
:contexts (box row column flex grid) :group geometry
|
||||||
|
:dirty-kind geometry :signature layout
|
||||||
|
:size-axis block :validator signed-size :runtime-default t)
|
||||||
|
(:name :z-index :id ebox/z-index :initial 0
|
||||||
|
:contexts (box row column flex grid) :group geometry
|
||||||
|
:dirty-kind geometry :signature layout :validator integer
|
||||||
|
:runtime-default t)
|
||||||
|
(:name :layer :id ebox/layer :initial local
|
||||||
|
:contexts (box row column flex grid) :group geometry
|
||||||
|
:dirty-kind geometry :signature layout :validator layer
|
||||||
|
:runtime-default t)
|
||||||
|
(:name :anchor :id ebox/anchor :initial nil
|
||||||
|
:contexts (box row column flex grid) :group geometry
|
||||||
|
:dirty-kind geometry :signature layout :validator anchor
|
||||||
|
:runtime-default t)
|
||||||
|
(:name :placement :id ebox/placement :initial bottom-start
|
||||||
|
:contexts (box row column flex grid) :group geometry
|
||||||
|
:dirty-kind geometry :signature layout :validator placement
|
||||||
|
:runtime-default t)
|
||||||
(:name :width :id ebox/width :initial auto
|
(:name :width :id ebox/width :initial auto
|
||||||
:contexts (box row column flex grid)
|
:contexts (box row column flex grid)
|
||||||
:group geometry :dirty-kind geometry :signature layout
|
:group geometry :dirty-kind geometry :signature layout
|
||||||
@ -904,6 +932,34 @@ the invalid value and its allowed units instead of a shorthand arity error."
|
|||||||
"Return non-nil when VALUE is an Ebox outer participation value."
|
"Return non-nil when VALUE is an Ebox outer participation value."
|
||||||
(memq value '(inline block)))
|
(memq value '(inline block)))
|
||||||
|
|
||||||
|
(defun ebox-style--signed-size-p (value &optional axis)
|
||||||
|
"Return non-nil when VALUE is a signed typed length in size AXIS.
|
||||||
|
Leave fractional and contextual lengths unresolved until used placement."
|
||||||
|
(and (ebox-size-value-p value t)
|
||||||
|
(let ((units (cdr (assq (or axis 'length) ebox-size-axis-units))))
|
||||||
|
(cl-labels ((valid-p (expression)
|
||||||
|
(or (atom expression)
|
||||||
|
(if (memq (car expression) '(px ch vw lh vh %))
|
||||||
|
(memq (car expression) units)
|
||||||
|
(cl-every #'valid-p (cdr expression))))))
|
||||||
|
(valid-p value)))))
|
||||||
|
|
||||||
|
(defun ebox-style--position-p (value)
|
||||||
|
"Return non-nil when VALUE is a supported positioning mode."
|
||||||
|
(memq value '(static relative absolute)))
|
||||||
|
|
||||||
|
(defun ebox-style--layer-p (value)
|
||||||
|
"Return non-nil when VALUE is a supported layer paint target."
|
||||||
|
(memq value '(local root)))
|
||||||
|
|
||||||
|
(defun ebox-style--anchor-p (value)
|
||||||
|
"Return non-nil when VALUE is nil or a semantic symbol or string id."
|
||||||
|
(or (symbolp value) (stringp value)))
|
||||||
|
|
||||||
|
(defun ebox-style--placement-p (value)
|
||||||
|
"Return non-nil when VALUE is a supported anchor placement."
|
||||||
|
(memq value '(bottom-start bottom-end top-start top-end)))
|
||||||
|
|
||||||
(defun ebox-style--font-family-p (value)
|
(defun ebox-style--font-family-p (value)
|
||||||
"Return non-nil when VALUE is an Ebox font family."
|
"Return non-nil when VALUE is an Ebox font family."
|
||||||
(or (null value)
|
(or (null value)
|
||||||
@ -1085,6 +1141,12 @@ generated wrappers that already contain exact line breaks."
|
|||||||
('positive-integer #'ebox-style--positive-integer-p)
|
('positive-integer #'ebox-style--positive-integer-p)
|
||||||
('grid-placement #'ebox-style--grid-placement-p)
|
('grid-placement #'ebox-style--grid-placement-p)
|
||||||
('outer #'ebox-style--outer-p)
|
('outer #'ebox-style--outer-p)
|
||||||
|
('signed-size #'ebox-style--signed-size-p)
|
||||||
|
('position #'ebox-style--position-p)
|
||||||
|
('integer #'integerp)
|
||||||
|
('layer #'ebox-style--layer-p)
|
||||||
|
('anchor #'ebox-style--anchor-p)
|
||||||
|
('placement #'ebox-style--placement-p)
|
||||||
('font-family #'ebox-style--font-family-p)
|
('font-family #'ebox-style--font-family-p)
|
||||||
('color #'ebox-style--color-p)
|
('color #'ebox-style--color-p)
|
||||||
('font-size #'ebox-style--font-size-p)
|
('font-size #'ebox-style--font-size-p)
|
||||||
@ -1162,8 +1224,10 @@ Grid track wrappers are traversed after their structural validator succeeds."
|
|||||||
(ebox-size-value-for-axis-p entry axis))
|
(ebox-size-value-for-axis-p entry axis))
|
||||||
(t (cl-every #'valid-p entry)))))
|
(t (cl-every #'valid-p entry)))))
|
||||||
(valid-p value))
|
(valid-p value))
|
||||||
(or (symbolp value)
|
(if (eq (plist-get property :validator) 'signed-size)
|
||||||
(ebox-size-value-for-axis-p value axis))))))
|
(ebox-style--signed-size-p value axis)
|
||||||
|
(or (symbolp value)
|
||||||
|
(ebox-size-value-for-axis-p value axis)))))))
|
||||||
|
|
||||||
(defun ebox-style--schema-validator (property)
|
(defun ebox-style--schema-validator (property)
|
||||||
"Return PROPERTY's grammar and unit-domain validator."
|
"Return PROPERTY's grammar and unit-domain validator."
|
||||||
@ -1243,7 +1307,7 @@ Grid track wrappers are traversed after their structural validator succeeds."
|
|||||||
(if (memq (plist-get property :name)
|
(if (memq (plist-get property :name)
|
||||||
'(:wrap-mode :item-gap :cross-align
|
'(:wrap-mode :item-gap :cross-align
|
||||||
:border-top-p :grid-column-span
|
:border-top-p :grid-column-span
|
||||||
:grid-row-span))
|
:grid-row-span :layer :anchor :placement))
|
||||||
'ebox
|
'ebox
|
||||||
'css)))
|
'css)))
|
||||||
(setq metadata
|
(setq metadata
|
||||||
@ -1439,6 +1503,12 @@ not affect declaration compilation and are intentionally excluded."
|
|||||||
('min-vertical-size (not (ebox-style--min-vertical-size-p value)))
|
('min-vertical-size (not (ebox-style--min-vertical-size-p value)))
|
||||||
('max-vertical-size (not (ebox-style--max-vertical-size-p value)))
|
('max-vertical-size (not (ebox-style--max-vertical-size-p value)))
|
||||||
('nonnegative-size (not (ebox-style--nonnegative-size-p value)))
|
('nonnegative-size (not (ebox-style--nonnegative-size-p value)))
|
||||||
|
('signed-size (not (ebox-style--signed-size-p value)))
|
||||||
|
('position (not (ebox-style--position-p value)))
|
||||||
|
('integer (not (integerp value)))
|
||||||
|
('layer (not (ebox-style--layer-p value)))
|
||||||
|
('anchor (not (ebox-style--anchor-p value)))
|
||||||
|
('placement (not (ebox-style--placement-p value)))
|
||||||
('positive-integer
|
('positive-integer
|
||||||
(and (numberp value) (not (ebox-style--positive-integer-p value))))
|
(and (numberp value) (not (ebox-style--positive-integer-p value))))
|
||||||
('grid-placement
|
('grid-placement
|
||||||
@ -1590,16 +1660,17 @@ When STRICT is non-nil, reject properties outside the Ebox style domain."
|
|||||||
declarations)
|
declarations)
|
||||||
|
|
||||||
(defun ebox-style--validate-author-values (plist)
|
(defun ebox-style--validate-author-values (plist)
|
||||||
"Reject explicit author values outside the public property grammars.
|
"Reject values in author PLIST outside the public property grammars.
|
||||||
An ECSS schema may use nil as an internal initial value, but nil is not an
|
An ECSS schema may use nil as an internal initial value, but nil is not an
|
||||||
author value unless the public grammar is boolean."
|
author value unless the public grammar is boolean or an optional anchor."
|
||||||
(unless (ebox-style--valid-plist-p plist)
|
(unless (ebox-style--valid-plist-p plist)
|
||||||
(user-error "Ebox style declarations must be an even property list"))
|
(user-error "Ebox style declarations must be an even property list"))
|
||||||
(cl-loop for (name value) on plist by #'cddr
|
(cl-loop for (name value) on plist by #'cddr
|
||||||
for property = (ebox-style--property name)
|
for property = (ebox-style--property name)
|
||||||
when (and property
|
when (and property
|
||||||
(null value)
|
(null value)
|
||||||
(not (eq (plist-get property :validator) 'boolean)))
|
(not (memq (plist-get property :validator)
|
||||||
|
'(boolean anchor))))
|
||||||
do (error "ebox: invalid value for %S: nil"
|
do (error "ebox: invalid value for %S: nil"
|
||||||
(plist-get property :name)))
|
(plist-get property :name)))
|
||||||
plist)
|
plist)
|
||||||
@ -2012,11 +2083,16 @@ explicit generation SOURCE-INDEX; facts are never recovered from its handle."
|
|||||||
(ebox-style--custom-property-p property))
|
(ebox-style--custom-property-p property))
|
||||||
append (list property value)))
|
append (list property value)))
|
||||||
|
|
||||||
|
(defvar ebox-style--box-runtime-defaults)
|
||||||
|
|
||||||
(defun ebox-style--reset-box-engine-style (box)
|
(defun ebox-style--reset-box-engine-style (box)
|
||||||
"Reset BOX engine style fields to their declared defaults."
|
"Reset BOX engine style fields to their declared defaults."
|
||||||
(cl-loop for (property value) on ebox--longhand by #'cddr
|
(cl-loop for (property value) on ebox--longhand by #'cddr
|
||||||
unless (memq property '(:content :scroll-offset))
|
unless (memq property '(:content :scroll-offset))
|
||||||
do (plist-put box property (copy-tree value)))
|
do (plist-put box property (copy-tree value)))
|
||||||
|
(unless (eq (plist-get box :ebox-kind) 'text)
|
||||||
|
(cl-loop for (property value) on ebox-style--box-runtime-defaults by #'cddr
|
||||||
|
do (plist-put box property (copy-tree value))))
|
||||||
box)
|
box)
|
||||||
|
|
||||||
(defun ebox-style--apply-engine-values (box values)
|
(defun ebox-style--apply-engine-values (box values)
|
||||||
@ -2292,6 +2368,9 @@ owned by NEW; only fields owned by the unchanged computed style are shared."
|
|||||||
(cl-loop for (property _value) on ebox--longhand by #'cddr
|
(cl-loop for (property _value) on ebox--longhand by #'cddr
|
||||||
unless (memq property '(:content :scroll-offset))
|
unless (memq property '(:content :scroll-offset))
|
||||||
do (ebox-style--transfer-node-property old new property))
|
do (ebox-style--transfer-node-property old new property))
|
||||||
|
(when (eq (plist-get new :ebox-kind) 'box)
|
||||||
|
(cl-loop for (property _value) on ebox-style--box-runtime-defaults by #'cddr
|
||||||
|
do (ebox-style--transfer-node-property old new property)))
|
||||||
(dolist (property ebox-style--item-projection-properties)
|
(dolist (property ebox-style--item-projection-properties)
|
||||||
(ebox-style--transfer-node-property old new property))
|
(ebox-style--transfer-node-property old new property))
|
||||||
new)
|
new)
|
||||||
@ -2470,6 +2549,16 @@ used geometry and become zero when the corresponding style is not `solid'."
|
|||||||
(ebox-style--apply-border-used-widths
|
(ebox-style--apply-border-used-widths
|
||||||
(ebox-style--validate-edge-longhands expanded))))
|
(ebox-style--validate-edge-longhands expanded))))
|
||||||
|
|
||||||
|
(defconst ebox-style--box-runtime-defaults
|
||||||
|
(ebox-style--expand-engine-plist
|
||||||
|
(cl-loop for property in ebox-style--property-definitions
|
||||||
|
when (plist-get property :runtime-default)
|
||||||
|
append (list (plist-get property :name)
|
||||||
|
(copy-tree (plist-get property :initial)))))
|
||||||
|
"Schema-owned defaults for additional Box runtime style fields.
|
||||||
|
Reset and source-stable transfer share this registration. Canonical Text
|
||||||
|
does not materialize these box-only placement facts.")
|
||||||
|
|
||||||
(defun ebox-style--border-delta-value
|
(defun ebox-style--border-delta-value
|
||||||
(declarations full-declarations style name)
|
(declarations full-declarations style name)
|
||||||
"Return border NAME from delta, complete declarations, STYLE, or initial."
|
"Return border NAME from delta, complete declarations, STYLE, or initial."
|
||||||
|
|||||||
@ -38,6 +38,8 @@
|
|||||||
(defvar ebox--viewport-dependent-subtree-cache)
|
(defvar ebox--viewport-dependent-subtree-cache)
|
||||||
(defvar ebox--viewport-height-dependent-subtree-cache)
|
(defvar ebox--viewport-height-dependent-subtree-cache)
|
||||||
(defvar ebox--render-runtime-revision)
|
(defvar ebox--render-runtime-revision)
|
||||||
|
(defvar ebox--render-source-index)
|
||||||
|
(defvar ebox-layer--active-p)
|
||||||
(defvar ebox--render-root-parent-kind)
|
(defvar ebox--render-root-parent-kind)
|
||||||
(defvar ebox--render-cache-table)
|
(defvar ebox--render-cache-table)
|
||||||
(defvar ebox--render-cache-signature-cache)
|
(defvar ebox--render-cache-signature-cache)
|
||||||
@ -2386,6 +2388,7 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
|
|||||||
success native-settled-p)
|
success native-settled-p)
|
||||||
(when (and (null on-mismatch)
|
(when (and (null on-mismatch)
|
||||||
(or (plist-get state-overrides :owner-scoped-proofs)
|
(or (plist-get state-overrides :owner-scoped-proofs)
|
||||||
|
(plist-get report-base :layer-recompose-p)
|
||||||
(plist-get state-overrides
|
(plist-get state-overrides
|
||||||
:scroll-state-transaction)
|
:scroll-state-transaction)
|
||||||
(> (length scope-node-ids) 1)))
|
(> (length scope-node-ids) 1)))
|
||||||
@ -3284,6 +3287,15 @@ available during planning; replacements here belong only to the candidate."
|
|||||||
(ebox--smooth-scroll-state-table smooth-table)
|
(ebox--smooth-scroll-state-table smooth-table)
|
||||||
(ebox--render-runtime-revision
|
(ebox--render-runtime-revision
|
||||||
(plist-get state :runtime-revision))
|
(plist-get state :runtime-revision))
|
||||||
|
(ebox--render-source-index (plist-get state :source-index))
|
||||||
|
(ebox-incremental--render-portals-p
|
||||||
|
(if (plist-member state :root-portals-p)
|
||||||
|
(plist-get state :root-portals-p)
|
||||||
|
'unknown))
|
||||||
|
(ebox-layer--active-p
|
||||||
|
(if (plist-member state :layered-p)
|
||||||
|
(plist-get state :layered-p)
|
||||||
|
'unknown))
|
||||||
(ebox--render-cache-table (plist-get state :render-cache))
|
(ebox--render-cache-table (plist-get state :render-cache))
|
||||||
(ebox--layout-fragments-table
|
(ebox--layout-fragments-table
|
||||||
(plist-get state :layout-fragments))
|
(plist-get state :layout-fragments))
|
||||||
@ -4150,6 +4162,42 @@ Proof mismatches return nil; unexpected rendering errors propagate."
|
|||||||
:external new-external :parent new-parent :roles new-roles
|
:external new-external :parent new-parent :roles new-roles
|
||||||
:overflow new-overflow :variable-content-p nil))))))
|
:overflow new-overflow :variable-content-p nil))))))
|
||||||
|
|
||||||
|
(defun ebox-surface--layer-patch-candidate (buffer previous-state state proof)
|
||||||
|
"Return PROOF's composed host replacement with only newly visible roles.
|
||||||
|
Render candidate STATE against BUFFER and its PREVIOUS-STATE.
|
||||||
|
Role topology can change inside the host. The Range footprint validator
|
||||||
|
already allows that change while requiring identical external allocation.
|
||||||
|
Enclosing effects must be unchanged; retained owner roles restore their paint
|
||||||
|
contributions without copying the previous children's hidden properties."
|
||||||
|
(let ((owner-id (plist-get proof :owner-id)))
|
||||||
|
(when (and (ebox-surface--unchanged-enclosing-effects-p
|
||||||
|
previous-state state owner-id)
|
||||||
|
;; Arbitrary ancestor surface properties need a separate
|
||||||
|
;; provenance proof; retain the existing full-render fallback.
|
||||||
|
(let ((parent (ebox-runtime-index-get
|
||||||
|
owner-id (plist-get state :parent-table)))
|
||||||
|
(valid t))
|
||||||
|
(while (and valid parent)
|
||||||
|
(when (ebox-get (ebox-runtime-index-get
|
||||||
|
parent (plist-get state :node-table))
|
||||||
|
:surface-properties)
|
||||||
|
(setq valid nil))
|
||||||
|
(setq parent (ebox-runtime-index-get
|
||||||
|
parent (plist-get state :parent-table))))
|
||||||
|
valid))
|
||||||
|
;; Paint-only preparation normally reuses visible descendant styles.
|
||||||
|
;; Occluded descendants have no visible fragments to refresh, so prepare
|
||||||
|
;; the isolated host's complete style closure before rendering it.
|
||||||
|
(when-let* ((node (ebox-runtime-index-get
|
||||||
|
owner-id (plist-get state :node-table))))
|
||||||
|
(ebox-surface-prepare-inline-candidate-styles
|
||||||
|
previous-state state (ebox--runtime-node-ids node))
|
||||||
|
(dolist (node-id (ebox--runtime-node-ids node))
|
||||||
|
(when-let* ((child (ebox-runtime-index-get
|
||||||
|
node-id (plist-get state :node-table))))
|
||||||
|
(remhash child (plist-get state :render-signature-cache)))))
|
||||||
|
(ebox-surface--range-patch-candidate buffer state proof))))
|
||||||
|
|
||||||
(defun ebox-surface--scroll-replace-index-lines (index changes)
|
(defun ebox-surface--scroll-replace-index-lines (index changes)
|
||||||
"Copy INDEX and replace only entries on CHANGES' cached content lines.
|
"Copy INDEX and replace only entries on CHANGES' cached content lines.
|
||||||
Each change is (LINE-NUMBER OLD-LINE NEW-LINE). Line counts are unchanged,
|
Each change is (LINE-NUMBER OLD-LINE NEW-LINE). Line counts are unchanged,
|
||||||
@ -4328,11 +4376,15 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
(if proofs
|
(if proofs
|
||||||
(mapcar
|
(mapcar
|
||||||
(lambda (proof)
|
(lambda (proof)
|
||||||
(if (plist-get proof :range-splice-p)
|
(cond
|
||||||
|
((plist-get proof :layer-recompose-p)
|
||||||
|
(ebox-surface--layer-patch-candidate
|
||||||
|
buffer previous-state state proof))
|
||||||
|
((plist-get proof :range-splice-p)
|
||||||
(unless (ebox-surface--owner-needs-ancestor-paint-p
|
(unless (ebox-surface--owner-needs-ancestor-paint-p
|
||||||
state (plist-get proof :owner-id))
|
state (plist-get proof :owner-id))
|
||||||
(ebox-surface--range-patch-candidate buffer state proof))
|
(ebox-surface--range-patch-candidate buffer state proof)))
|
||||||
(ebox-surface--owner-patch-candidate
|
(t (ebox-surface--owner-patch-candidate
|
||||||
buffer state (plist-get proof :owner-id)
|
buffer state (plist-get proof :owner-id)
|
||||||
(plist-get proof :allocated-width)
|
(plist-get proof :allocated-width)
|
||||||
(plist-get proof :variable-content-p)
|
(plist-get proof :variable-content-p)
|
||||||
@ -4341,7 +4393,7 @@ local; a declined visible proof leaves the committed scroll prefix untouched."
|
|||||||
(plist-get proof :changed-keys)
|
(plist-get proof :changed-keys)
|
||||||
(plist-get proof :allocation-closure-p)
|
(plist-get proof :allocation-closure-p)
|
||||||
(plist-get proof :retain-external-owner-suffix-p)
|
(plist-get proof :retain-external-owner-suffix-p)
|
||||||
(and (= (length proofs) 1) previous-state))))
|
(and (= (length proofs) 1) previous-state)))))
|
||||||
proofs)
|
proofs)
|
||||||
(list
|
(list
|
||||||
(ebox-surface--owner-patch-candidate
|
(ebox-surface--owner-patch-candidate
|
||||||
@ -6223,6 +6275,21 @@ but cannot introduce or remove a viewport expression or node identity."
|
|||||||
(plist-put state :fractional-pixel-output-p
|
(plist-put state :fractional-pixel-output-p
|
||||||
(plist-get previous-state :fractional-pixel-output-p))
|
(plist-get previous-state :fractional-pixel-output-p))
|
||||||
(plist-put state :root-node root)
|
(plist-put state :root-node root)
|
||||||
|
(plist-put state :layered-p
|
||||||
|
(if projection-kind
|
||||||
|
(or (plist-get previous-state :layered-p)
|
||||||
|
(cl-some (lambda (proof)
|
||||||
|
(plist-get proof :layer-recompose-p))
|
||||||
|
(plist-get state :owner-scoped-proofs)))
|
||||||
|
(and (fboundp 'ebox-layer-subtree-p)
|
||||||
|
(ebox-layer-subtree-p root))))
|
||||||
|
(plist-put state :root-portals-p
|
||||||
|
(if projection-kind
|
||||||
|
(or (plist-get previous-state :root-portals-p)
|
||||||
|
(and (plist-get state :layer-recompose-p)
|
||||||
|
(ebox-layer-portals-p root)))
|
||||||
|
(and (fboundp 'ebox-layer-portals-p)
|
||||||
|
(ebox-layer-portals-p root))))
|
||||||
(when (plist-get state :native-sync-confirmed-p)
|
(when (plist-get state :native-sync-confirmed-p)
|
||||||
(ebox-native-commit-attach-confirmed-base previous-state state))
|
(ebox-native-commit-attach-confirmed-base previous-state state))
|
||||||
(plist-put state :projection-kind projection-kind)
|
(plist-put state :projection-kind projection-kind)
|
||||||
|
|||||||
3
ebox.el
3
ebox.el
@ -35,7 +35,8 @@
|
|||||||
"ebox-layout-config.el"
|
"ebox-layout-config.el"
|
||||||
"ebox-node-factory.el"
|
"ebox-node-factory.el"
|
||||||
"ebox-child-range.el" "ebox-tree.el" "ebox-measure.el"
|
"ebox-child-range.el" "ebox-tree.el" "ebox-measure.el"
|
||||||
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
|
"ebox-fragment.el" "ebox-render-context.el" "ebox-composite.el"
|
||||||
|
"ebox-layer.el" "ebox-layout.el"
|
||||||
"ebox-flex.el" "ebox-grid.el" "ebox-canonical.el"
|
"ebox-flex.el" "ebox-grid.el" "ebox-canonical.el"
|
||||||
"ebox-buffer-backend.el" "ebox-patch-plan.el" "ebox-incremental.el"
|
"ebox-buffer-backend.el" "ebox-patch-plan.el" "ebox-incremental.el"
|
||||||
"ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el"
|
"ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el"
|
||||||
|
|||||||
286
tests/ebox-composite-tests.el
Normal file
286
tests/ebox-composite-tests.el
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
;;; ebox-composite-tests.el --- Pixel row compositor tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Independent deterministic display metrics exercise cuts and coverage without
|
||||||
|
;; requiring a GUI frame. Production measurement still handles EQ display runs.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ebox)
|
||||||
|
(require 'ebox-composite)
|
||||||
|
|
||||||
|
(defun ebox-composite-test--plain-width (text)
|
||||||
|
"Measure TEXT with seven-pixel glyphs, zero-width marks, and fi shaping."
|
||||||
|
(let ((width 0) (position 0))
|
||||||
|
(dolist (char (string-to-list text))
|
||||||
|
(unless (or (memq (get-char-code-property char 'general-category)
|
||||||
|
'(Mn Mc Me))
|
||||||
|
(= char #x200d)
|
||||||
|
(and (>= char #x1f3fb) (<= char #x1f3ff)))
|
||||||
|
(cl-incf width 7)))
|
||||||
|
(while (string-match "fi" text position)
|
||||||
|
(cl-decf width 3)
|
||||||
|
(setq position (match-end 0)))
|
||||||
|
width))
|
||||||
|
|
||||||
|
(defun ebox-composite-test--pixel-width (text)
|
||||||
|
"Return TEXT's deterministic width, replacing each EQ display run once."
|
||||||
|
(let ((position 0) (width 0) (end (length text)))
|
||||||
|
(while (< position end)
|
||||||
|
(let* ((display (get-text-property position 'display text))
|
||||||
|
(next (next-single-property-change position 'display text end)))
|
||||||
|
(cl-incf
|
||||||
|
width
|
||||||
|
(cond
|
||||||
|
((and (consp display) (eq (car display) 'space))
|
||||||
|
(car (plist-get (cdr display) :width)))
|
||||||
|
((and (consp display) (eq (car display) 'image))
|
||||||
|
(plist-get (cdr display) :width))
|
||||||
|
((stringp display) (ebox-composite-test--pixel-width display))
|
||||||
|
(t (ebox-composite-test--plain-width
|
||||||
|
(substring-no-properties text position next)))))
|
||||||
|
(setq position next)))
|
||||||
|
width))
|
||||||
|
|
||||||
|
(defmacro ebox-composite-test--with-metrics (&rest body)
|
||||||
|
"Run BODY using the independent pixel oracle and fresh measurement caches."
|
||||||
|
(declare (indent 0) (debug t))
|
||||||
|
`(cl-letf (((symbol-function 'string-pixel-width)
|
||||||
|
#'ebox-composite-test--pixel-width))
|
||||||
|
(ebox-clear-cache)
|
||||||
|
(unwind-protect (progn ,@body)
|
||||||
|
(ebox-clear-cache))))
|
||||||
|
|
||||||
|
(defun ebox-composite-test--assert-width (text width)
|
||||||
|
"Assert TEXT is WIDTH pixels in the production and independent metrics."
|
||||||
|
(should (= (ebox--string-pixel-width text) width))
|
||||||
|
(should (= (ebox-composite-test--pixel-width text) width)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-slice-keeps-original-coordinates ()
|
||||||
|
"Partial edge glyphs leave neutral blanks and do not shift retained text."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let* ((keymap (make-sparse-keymap))
|
||||||
|
(owner (list 'owner))
|
||||||
|
(source (propertize "ABCD" 'keymap keymap 'ebox-owner owner)))
|
||||||
|
(dotimes (index (length source))
|
||||||
|
(put-text-property index (1+ index) 'source-position index source))
|
||||||
|
(let ((output (ebox-composite-slice source 3 24)))
|
||||||
|
(should (equal (substring-no-properties output) " BC "))
|
||||||
|
(ebox-composite-test--assert-width output 21)
|
||||||
|
(should-not (get-text-property 0 'keymap output))
|
||||||
|
(should-not (get-text-property 3 'keymap output))
|
||||||
|
(should-not (get-text-property 0 'ebox-owner output))
|
||||||
|
(should (eq (get-text-property 1 'keymap output) keymap))
|
||||||
|
(should (eq (get-text-property 2 'ebox-owner output) owner))
|
||||||
|
(should (= (get-text-property 1 'source-position output) 1))
|
||||||
|
(should (= (get-text-property 2 'source-position output) 2))
|
||||||
|
(should (= (ebox--string-pixel-width (substring output 0 1)) 4))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-slice-fills-exhausted-and-empty-source ()
|
||||||
|
"Intervals beyond source content retain their exact neutral canvas width."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(dolist (case '((nil 0 21) ("" 3 10) ("A" 10 21) ("A" 3 21)))
|
||||||
|
(pcase-let ((`(,source ,start ,end) case))
|
||||||
|
(let ((output (ebox-composite-slice source start end)))
|
||||||
|
(ebox-composite-test--assert-width output (- end start))
|
||||||
|
(should (string-blank-p output))
|
||||||
|
(should-not (text-property-not-all 0 (length output)
|
||||||
|
'keymap nil output)))))
|
||||||
|
(should (equal (ebox-composite-slice "abc" 2 2) ""))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-slice-keeps-supported-graphemes-whole ()
|
||||||
|
"Both cut edges preserve combining, variation, modifier, ZWJ, and flags."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(dolist (cluster '("é" "✈️" "👍🏽" "👩💻" "🇨🇳"))
|
||||||
|
(let* ((source (concat "A" cluster "Z"))
|
||||||
|
(right (+ 7 (ebox--string-pixel-width cluster)))
|
||||||
|
(whole (ebox-composite-slice source 7 right))
|
||||||
|
(cut-left (ebox-composite-slice source 8 (+ right 7)))
|
||||||
|
(cut-right (ebox-composite-slice source 0 (1- right))))
|
||||||
|
(should (equal (substring-no-properties whole) cluster))
|
||||||
|
(should (equal (substring-no-properties cut-left) " Z"))
|
||||||
|
(should (equal (substring-no-properties cut-right) "A "))
|
||||||
|
(ebox-composite-test--assert-width whole (- right 7))
|
||||||
|
(ebox-composite-test--assert-width cut-left (- right 1))
|
||||||
|
(ebox-composite-test--assert-width cut-right (1- right))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-slice-preserves-whole-prefix-shaping ()
|
||||||
|
"A shaped fitting prefix is retained, and a detached glyph cannot spill."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let ((whole (ebox-composite-slice "fiZ" 0 11))
|
||||||
|
(suffix (ebox-composite-slice "fiZ" 7 18)))
|
||||||
|
(should (equal whole "fi"))
|
||||||
|
(ebox-composite-test--assert-width whole 11)
|
||||||
|
(should (equal (substring-no-properties suffix) " Z"))
|
||||||
|
(should (= (ebox--string-pixel-width (substring suffix 0 1)) 4))
|
||||||
|
(ebox-composite-test--assert-width suffix 11))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-slice-cuts-grouped-fractional-spaces ()
|
||||||
|
"A shared space can be cut on both ends without copying nested properties."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let* ((height (list 20))
|
||||||
|
(ascent (list 80))
|
||||||
|
(display (list 'space :width '(15.5) :height height :ascent ascent))
|
||||||
|
(keymap (make-sparse-keymap))
|
||||||
|
(face (list :foreground "red"))
|
||||||
|
(source (concat (propertize "abc" 'display display
|
||||||
|
'keymap keymap 'face face) "Z"))
|
||||||
|
(before (copy-sequence source))
|
||||||
|
(output (ebox-composite-slice source 2.25 13.75))
|
||||||
|
(result-display (get-text-property 0 'display output))
|
||||||
|
(whole (ebox-composite-slice source 0 15.5)))
|
||||||
|
(ebox-composite-test--assert-width output 11.5)
|
||||||
|
(should (equal (substring-no-properties output) "abc"))
|
||||||
|
(should (= (car (plist-get (cdr result-display) :width)) 11.5))
|
||||||
|
(should (eq (plist-get (cdr result-display) :height) height))
|
||||||
|
(should (eq (plist-get (cdr result-display) :ascent) ascent))
|
||||||
|
(dotimes (index (length output))
|
||||||
|
(should (eq (get-text-property index 'display output) result-display))
|
||||||
|
(should (eq (get-text-property index 'keymap output) keymap))
|
||||||
|
(should (eq (get-text-property index 'face output) face)))
|
||||||
|
(should (eq (get-text-property 0 'display whole) display))
|
||||||
|
(should (equal-including-properties before source))
|
||||||
|
(should (equal (plist-get (cdr display) :width) '(15.5))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-slice-never-splits-replacement-runs ()
|
||||||
|
"Shared string and image replacements are kept whole, even across faces."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(dolist (display (list (copy-sequence "WXYZ")
|
||||||
|
(list 'image :type 'xpm :width 28)))
|
||||||
|
(let* ((keymap (make-sparse-keymap))
|
||||||
|
(source (propertize "abc" 'display display 'keymap keymap)))
|
||||||
|
(put-text-property 1 2 'face 'bold source)
|
||||||
|
(let ((whole (ebox-composite-slice source 0 28))
|
||||||
|
(middle (ebox-composite-slice source 7 21)))
|
||||||
|
(should (equal-including-properties source whole))
|
||||||
|
(should (eq (get-text-property 2 'display whole) display))
|
||||||
|
(should (string-blank-p middle))
|
||||||
|
(should-not (get-text-property 0 'keymap middle))
|
||||||
|
(ebox-composite-test--assert-width whole 28)
|
||||||
|
(ebox-composite-test--assert-width middle 14))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-line-resolves-overlapping-opaque-a-b-c ()
|
||||||
|
"The last placement wins, including its blank pixels, at stable positions."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let* ((base "0123456789")
|
||||||
|
(placements (list (list :text "AAAAAA" :x 7)
|
||||||
|
(list :text "BBBB" :x 21)
|
||||||
|
(list :text " C " :x 28)))
|
||||||
|
(output (ebox-composite-line base placements 70)))
|
||||||
|
(should (equal (substring-no-properties output) "0AAB C 789"))
|
||||||
|
(ebox-composite-test--assert-width output 70)
|
||||||
|
(should (equal base "0123456789"))
|
||||||
|
(should (equal placements '((:text "AAAAAA" :x 7)
|
||||||
|
(:text "BBBB" :x 21)
|
||||||
|
(:text " C " :x 28)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-line-preserves-underlay-right-coordinates ()
|
||||||
|
"Cutting an underlay glyph cannot pull its right-hand neighbor leftward."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let ((output (ebox-composite-line
|
||||||
|
"ABCD" (list (list :text (ebox-pixel-space 10) :x 3)) 28)))
|
||||||
|
(should (equal (substring-no-properties output) " CD"))
|
||||||
|
(ebox-composite-test--assert-width output 28)
|
||||||
|
(should (= (ebox--string-pixel-width
|
||||||
|
(substring output 0 (string-match "C" output))) 14)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-line-handles-negative-and-clipped-placements ()
|
||||||
|
"Canvas clipping retains original placement coordinates at both edges."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let ((output
|
||||||
|
(ebox-composite-line
|
||||||
|
"012345" (list (list :text "ABC" :x -10)
|
||||||
|
(list :text "XY" :x 35)
|
||||||
|
(list :text "hidden" :x 100)) 42)))
|
||||||
|
(should (equal (substring-no-properties output) " C 234X"))
|
||||||
|
(ebox-composite-test--assert-width output 42))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-line-hidden-boundaries-do-not-split-top-glyph ()
|
||||||
|
"A hidden lower placement cannot fragment a fully visible replacement."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let* ((display (copy-sequence "ABC"))
|
||||||
|
(top (propertize "source" 'display display))
|
||||||
|
(output (ebox-composite-line
|
||||||
|
nil (list (list :text (ebox-pixel-space 2) :x 4)
|
||||||
|
(list :text (ebox-pixel-space 3) :x 10)
|
||||||
|
(list :text top :x 0)) 21)))
|
||||||
|
(should (equal-including-properties output top))
|
||||||
|
(should (eq (get-text-property 0 'display output) display))
|
||||||
|
(ebox-composite-test--assert-width output 21))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-line-keeps-adjacent-eq-runs-separate ()
|
||||||
|
"Independent touching placements sharing a display still occupy two runs."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let* ((display (list 'space :width '(10)))
|
||||||
|
(keymap (make-sparse-keymap))
|
||||||
|
(text (propertize "ab" 'display display 'keymap keymap))
|
||||||
|
(output (ebox-composite-line
|
||||||
|
nil (list (list :text text :x 0)
|
||||||
|
(list :text text :x 10)) 20)))
|
||||||
|
(ebox-composite-test--assert-width output 20)
|
||||||
|
(should (eq (get-text-property 0 'display output) display))
|
||||||
|
(should (eq (get-text-property (1- (length output)) 'display output)
|
||||||
|
display))
|
||||||
|
(should-not (get-text-property 2 'keymap output))
|
||||||
|
(should (eq (get-text-property 3 'keymap output) keymap)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-line-keeps-independent-shaping-extents ()
|
||||||
|
"Neighboring sources cannot form a ligature that moves the right edge."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let ((output (ebox-composite-line
|
||||||
|
"fZ" (list (list :text "i" :x 7)) 14)))
|
||||||
|
(ebox-composite-test--assert-width output 14)
|
||||||
|
(should (equal (substring-no-properties output) "f i"))
|
||||||
|
(should (equal (get-text-property 1 'display output)
|
||||||
|
'(space :width (0)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-slice-full-line-keeps-zero-width-source-units ()
|
||||||
|
"A complete line retains even a zero-width source's display height."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let* ((display (list 'space :width '(0) :height '(24)))
|
||||||
|
(source (concat "A" (propertize "tail" 'display display)))
|
||||||
|
(output (ebox-composite-slice source 0 7)))
|
||||||
|
(should (equal-including-properties output source))
|
||||||
|
(should (eq (get-text-property 1 'display output) display))
|
||||||
|
(ebox-composite-test--assert-width output 7))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-line-uses-latest-base-after-placement-removal ()
|
||||||
|
"Composition retains no stale underlay state between successive calls."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let* ((placements (list (list :text "XX" :x 7)))
|
||||||
|
(old (ebox-composite-line "abcdef" placements 42))
|
||||||
|
(updated (ebox-composite-line "123456" placements 42))
|
||||||
|
(restored (ebox-composite-line "123456" nil 42)))
|
||||||
|
(should (equal old "aXXdef"))
|
||||||
|
(should (equal updated "1XX456"))
|
||||||
|
(should (equal restored "123456"))
|
||||||
|
(dolist (output (list old updated restored))
|
||||||
|
(ebox-composite-test--assert-width output 42)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-line-neutral-gaps-have-no-source-properties ()
|
||||||
|
"A short or absent base does not lend interaction properties to root fill."
|
||||||
|
(ebox-composite-test--with-metrics
|
||||||
|
(let* ((keymap (make-sparse-keymap))
|
||||||
|
(base (propertize "A" 'keymap keymap 'help-echo "base"))
|
||||||
|
(output (ebox-composite-line base nil 21)))
|
||||||
|
(ebox-composite-test--assert-width output 21)
|
||||||
|
(should (eq (get-text-property 0 'keymap output) keymap))
|
||||||
|
(should-not (get-text-property 1 'keymap output))
|
||||||
|
(should-not (get-text-property 1 'help-echo output)))
|
||||||
|
(ebox-composite-test--assert-width (ebox-composite-line nil nil 12.5) 12.5)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-composite-rejects-invalid-bounds-and-lines ()
|
||||||
|
"Invalid extents and multiline inputs fail before source mutation."
|
||||||
|
(dolist (bounds '((-1 2) (3 2) (nil 1) (0 nope) (0 1.0e+INF)))
|
||||||
|
(should-error (apply #'ebox-composite-slice "abc" bounds)))
|
||||||
|
(should-error (ebox-composite-slice "a\nb" 0 1))
|
||||||
|
(should-error (ebox-composite-line "a\nb" nil 10))
|
||||||
|
(should-error (ebox-composite-line nil '((:text "x" :x nope)) 10))
|
||||||
|
(should-error (ebox-composite-line nil '((:text "x\ny" :x 0)) 10))
|
||||||
|
(should-error (ebox-composite-line nil nil -1))
|
||||||
|
(should-error (ebox-composite-line nil '(broken . list) 10)))
|
||||||
|
|
||||||
|
(provide 'ebox-composite-tests)
|
||||||
|
;;; ebox-composite-tests.el ends here
|
||||||
302
tests/ebox-layer-publication-tests.el
Normal file
302
tests/ebox-layer-publication-tests.el
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
;;; ebox-layer-publication-tests.el --- Retained layer updates -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Exercise layer composition through public mounted update APIs. Hidden
|
||||||
|
;; source objects remain logical inputs, while only visible roles are mounted.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ebox)
|
||||||
|
(require 'ebox-selector)
|
||||||
|
|
||||||
|
(defmacro ebox-layer-publication-test--with-buffer (&rest body)
|
||||||
|
"Run BODY with an isolated buffer and the explicit Elisp backend."
|
||||||
|
(declare (indent 0) (debug t))
|
||||||
|
`(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
|
(lambda () nil)))
|
||||||
|
(let ((ebox-viewport-width 160) (ebox-viewport-height 10))
|
||||||
|
(with-temp-buffer ,@body))))
|
||||||
|
|
||||||
|
(defun ebox-layer-publication-test--input (&optional lower upper-visible)
|
||||||
|
"Build a nested fixed host with LOWER text and UPPER-VISIBLE overlay."
|
||||||
|
(ebox-build
|
||||||
|
`(column :id "root" :width (ch 12) :height (lh 5)
|
||||||
|
(box :id "before" "Before")
|
||||||
|
(box :id "host" :width (ch 8) :height (lh 2)
|
||||||
|
(box :id "lower" :width (ch 8) :height (lh 1)
|
||||||
|
,(or lower "LOWER001"))
|
||||||
|
(box :id "upper" :position absolute :left (px 0) :top (lh 0)
|
||||||
|
:width (ch 8) :height (lh 1) :z-index 1
|
||||||
|
:visibility ,(if upper-visible 'visible 'hidden)
|
||||||
|
"UPPER001"))
|
||||||
|
(box :id "after" "After"))))
|
||||||
|
|
||||||
|
(defun ebox-layer-publication-test--assert-fresh-render ()
|
||||||
|
"Check visible text against a fresh render of the committed logical input."
|
||||||
|
(should
|
||||||
|
(equal (buffer-substring-no-properties (point-min) (point-max))
|
||||||
|
(substring-no-properties
|
||||||
|
(ebox-render
|
||||||
|
(plist-get (ebox-surface-buffer-snapshot (current-buffer))
|
||||||
|
:input))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-hidden-updates-reveal-current-input ()
|
||||||
|
"Hidden content and paint updates survive without painting over upper text."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-publication-test--input nil t))
|
||||||
|
(let ((render (symbol-function 'ebox-surface--render-candidate))
|
||||||
|
(full-renders 0))
|
||||||
|
(should (string-match-p "UPPER001" (buffer-string)))
|
||||||
|
(should-not (string-match-p "LOWER001" (buffer-string)))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf full-renders)
|
||||||
|
(apply render arguments))))
|
||||||
|
(ebox-region-update "lower" :content "LOWER002")
|
||||||
|
(ebox-selector-update-buffer
|
||||||
|
(current-buffer) "#lower" :color "#ff0000")
|
||||||
|
(should (string-match-p "UPPER001" (buffer-string)))
|
||||||
|
(should-not (string-match-p "LOWER002" (buffer-string)))
|
||||||
|
(ebox-region-update "upper" :visibility 'hidden)
|
||||||
|
(should (string-match-p "LOWER002" (buffer-string)))
|
||||||
|
(should-not (string-match-p "UPPER001" (buffer-string)))
|
||||||
|
(let* ((start (string-match "LOWER002" (buffer-string)))
|
||||||
|
(face (get-text-property (+ (point-min) start) 'face)))
|
||||||
|
(should (string-match-p "#ff0000" (prin1-to-string face)))))
|
||||||
|
(should (= full-renders 0))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-rejected-hidden-commit-rolls-back ()
|
||||||
|
"Rejected hidden updates leave retained input, revision and text unchanged."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-publication-test--input nil t))
|
||||||
|
(let ((before (buffer-string))
|
||||||
|
(revision (ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(should-error
|
||||||
|
(ebox-commit
|
||||||
|
(current-buffer) (ebox-layer-publication-test--input "REJECTED" t)
|
||||||
|
(lambda (_report) (error "Reject hidden layer candidate"))))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(ebox-region-update "upper" :visibility 'hidden)
|
||||||
|
(should (string-match-p "LOWER001" (buffer-string)))
|
||||||
|
(should-not (string-match-p "REJECTED" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-footprint-change-falls-back ()
|
||||||
|
"Host geometry changes preserve siblings through the existing root fallback."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-publication-test--input nil t))
|
||||||
|
(let ((render (symbol-function 'ebox-surface--render-candidate))
|
||||||
|
(full-renders 0))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-incf full-renders)
|
||||||
|
(apply render arguments))))
|
||||||
|
(ebox-region-update "host" :height '(lh 3)))
|
||||||
|
(should (> full-renders 0))
|
||||||
|
(should (string-match-p "Before" (buffer-string)))
|
||||||
|
(should (string-match-p "After" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-ordinary-update-skips-layer-walk ()
|
||||||
|
"A non-layer tree's owner planner does not scan on content changes."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build '(column :width (ch 12) :height (lh 3)
|
||||||
|
(box :id "text" :width (ch 8) :height (lh 1) "BEFORE00"))))
|
||||||
|
(let ((plan (symbol-function 'ebox-incremental--layer-owner-plan)))
|
||||||
|
(cl-letf (((symbol-function 'ebox-incremental--layer-owner-plan)
|
||||||
|
(lambda (&rest arguments)
|
||||||
|
(cl-letf (((symbol-function 'ebox-layer-host-p)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "Unexpected layer host walk")))
|
||||||
|
((symbol-function 'ebox-layer-subtree-p)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "Unexpected layer subtree scan"))))
|
||||||
|
(apply plan arguments)))))
|
||||||
|
(ebox-region-update "text" :content "AFTER000")))
|
||||||
|
(should (string-match-p "AFTER000" (buffer-string)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-declines-unsupported-native-layout ()
|
||||||
|
"A retained layer tree is ineligible for both native layout entry points."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-publication-test--input nil t))
|
||||||
|
(let* ((state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(root (plist-get state :root-node)))
|
||||||
|
(should-not (ebox-native-reflow--native-node-supported-p root))
|
||||||
|
(should-not (ebox-native-commit--runtime-types-supported-p state))
|
||||||
|
(should-not (plist-get state :native-render-p)))
|
||||||
|
(ebox-region-update "lower" :content "LOWER002")
|
||||||
|
(ebox-region-update "upper" :visibility 'hidden)
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-visible-interactions-follow-occlusion ()
|
||||||
|
"Only the visible layer owns interaction properties after hidden updates."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-publication-test--input nil t))
|
||||||
|
(ebox-region-update "lower" :keymap '(keymap (13 . backward-char))
|
||||||
|
:help-echo "lower-old")
|
||||||
|
(ebox-region-update "upper" :keymap '(keymap (13 . forward-char))
|
||||||
|
:help-echo "upper")
|
||||||
|
(ebox-region-update "lower" :keymap '(keymap (13 . ignore))
|
||||||
|
:help-echo "lower-new")
|
||||||
|
(let ((position (+ (point-min) (string-match "UPPER001" (buffer-string)))))
|
||||||
|
(should (eq (lookup-key (get-text-property position 'keymap) (kbd "RET"))
|
||||||
|
#'forward-char))
|
||||||
|
(should (equal (get-text-property position 'help-echo) "upper")))
|
||||||
|
(ebox-region-update "upper" :visibility 'hidden)
|
||||||
|
(let ((position (+ (point-min) (string-match "LOWER001" (buffer-string)))))
|
||||||
|
(should (eq (lookup-key (get-text-property position 'keymap) (kbd "RET"))
|
||||||
|
#'ignore))
|
||||||
|
(should (equal (get-text-property position 'help-echo) "lower-new")))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-ancestor-paint-survives-recomposition ()
|
||||||
|
"A detached host retains enclosing paint and interaction contributions."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-publication-test--input nil t))
|
||||||
|
(ebox-region-update "root" :color "#00ff00" :background-color "#000080"
|
||||||
|
:help-echo "ancestor")
|
||||||
|
(ebox-region-update "lower" :content "LOWER002")
|
||||||
|
(ebox-region-update "upper" :visibility 'hidden)
|
||||||
|
(let* ((actual (buffer-string))
|
||||||
|
(expected (ebox-render
|
||||||
|
(plist-get (ebox-surface-buffer-snapshot (current-buffer))
|
||||||
|
:input)))
|
||||||
|
(actual-position (string-match "LOWER002" actual))
|
||||||
|
(expected-position (string-match "LOWER002" expected)))
|
||||||
|
(should (equal (get-text-property actual-position 'face actual)
|
||||||
|
(get-text-property expected-position 'face expected)))
|
||||||
|
(should (equal (get-text-property actual-position 'help-echo actual)
|
||||||
|
"ancestor")))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-batch-retains-disjoint-owner-updates ()
|
||||||
|
"A hidden update and an ordinary sibling update publish one current input."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-layer-publication-test--input nil t))
|
||||||
|
(ebox-incremental-begin-batch (current-buffer))
|
||||||
|
(ebox-region-update "lower" :content "LOWER002")
|
||||||
|
(ebox-region-update "after" :content "Later")
|
||||||
|
(ebox-incremental-flush (current-buffer))
|
||||||
|
(should (string-match-p "UPPER001" (buffer-string)))
|
||||||
|
(should (string-match-p "Later" (buffer-string)))
|
||||||
|
(should-not (string-match-p "LOWER002" (buffer-string)))
|
||||||
|
(ebox-region-update "upper" :visibility 'hidden)
|
||||||
|
(should (string-match-p "LOWER002" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-root-portal-occludes-unrelated-owner ()
|
||||||
|
"Updates outside a root portal's local host cannot paint over the portal."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build
|
||||||
|
'(column :id "root" :width (ch 12) :height (lh 5)
|
||||||
|
(box :id "body" :width (ch 8) :height (lh 1) "BODY0001")
|
||||||
|
(box :id "host" :width (ch 4) :height (lh 1)
|
||||||
|
(box :id "portal" :position absolute :layer root
|
||||||
|
:left (px 0) :top (lh 0) :width (ch 8) :height (lh 1)
|
||||||
|
:z-index 1 "PORTAL01")))))
|
||||||
|
(ebox-region-update "body" :content "BODY0002")
|
||||||
|
(should (string-match-p "PORTAL01" (buffer-string)))
|
||||||
|
(should-not (string-match-p "BODY0002" (buffer-string)))
|
||||||
|
(ebox-region-update "portal" :visibility 'hidden)
|
||||||
|
(should (string-match-p "BODY0002" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-root-portal-survives-cached-branch ()
|
||||||
|
"An unchanged cached branch still collects its root portal on later renders."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build
|
||||||
|
'(column :id "root" :width (ch 12) :height (lh 5)
|
||||||
|
(box :id "body" :width (ch 12) :height (lh 1) "BODY0001")
|
||||||
|
(box :id "host" :width (ch 4) :height (lh 1)
|
||||||
|
(box :id "local" :width (ch 4) :height (lh 1) "HOST")
|
||||||
|
(box :id "portal" :position absolute :layer root
|
||||||
|
:left (ch 4) :top (lh 3) :width (ch 6) :height (lh 1)
|
||||||
|
"PORTAL")))))
|
||||||
|
(dotimes (index 3)
|
||||||
|
(ebox-region-update "body" :content (format "BODY000%d" (+ index 2)))
|
||||||
|
(should (string-match-p "PORTAL" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render))
|
||||||
|
(ebox-region-update "portal" :visibility 'hidden)
|
||||||
|
(should-not (string-match-p "PORTAL" (buffer-string)))
|
||||||
|
(ebox-region-update "portal" :content "LATEST")
|
||||||
|
(ebox-region-update "portal" :visibility 'visible)
|
||||||
|
(should (string-match-p "LATEST" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-revealed-anchor-uses-candidate-source ()
|
||||||
|
"A revealed portal resolves its semantic anchor against current input."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build
|
||||||
|
'(column :id "root" :width (ch 12) :height (lh 5)
|
||||||
|
(box :id "trigger" :width (ch 8) :height (lh 1) "ANCHOR01")
|
||||||
|
(box :id "host" :width (ch 4) :height (lh 1)
|
||||||
|
(box :id "portal" :position absolute :layer root
|
||||||
|
:anchor "trigger" :width (ch 6) :height (lh 1)
|
||||||
|
:visibility hidden "PORTAL")))))
|
||||||
|
(ebox-region-update "portal" :visibility 'visible)
|
||||||
|
(should (string-match-p "PORTAL" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render)
|
||||||
|
(ebox-region-update "trigger" :position 'relative :left '(ch 2))
|
||||||
|
(should (string-match-p "PORTAL" (buffer-string)))
|
||||||
|
(ebox-layer-publication-test--assert-fresh-render)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-publication-scroll-under-overlay-and-rollback ()
|
||||||
|
"Scrolling lower content recomposes its host and rolls back with TP."
|
||||||
|
(ebox-layer-publication-test--with-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-build
|
||||||
|
'(column :id "root" :width (ch 12) :height (lh 5)
|
||||||
|
(box :id "before" "Before")
|
||||||
|
(box :id "host" :width (ch 8) :height (lh 2)
|
||||||
|
(box :id "scroll" :width (ch 8) :height (lh 2) :overflow scroll
|
||||||
|
"ZERO0000\nONE00000\nTWO00000\nTHREE000")
|
||||||
|
(box :id "upper" :position absolute :width (ch 8) :height (lh 1)
|
||||||
|
"UPPER001"))
|
||||||
|
(box :id "after" "After"))))
|
||||||
|
(let* ((region-id (plist-get
|
||||||
|
(car (ebox-selector-query-buffer (current-buffer) "#scroll"))
|
||||||
|
:region-id))
|
||||||
|
(revision (ebox-surface-buffer-revision (current-buffer)))
|
||||||
|
(before (buffer-string)))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "Fixed local layer scroll rendered the root"))))
|
||||||
|
(let ((tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step 'client-state) (error "Reject layer scroll")))))
|
||||||
|
(should-error (ebox--scroll-region-by region-id 1 1)))
|
||||||
|
(should (= revision (ebox-surface-buffer-revision (current-buffer))))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(should (= 0 (plist-get (ebox-scroll-state region-id) :scroll-offset)))
|
||||||
|
(should (= 1 (ebox--scroll-region-by region-id 1 1)))
|
||||||
|
(should (string-match-p "UPPER001" (buffer-string)))
|
||||||
|
(should (string-match-p "TWO00000" (buffer-string)))
|
||||||
|
(should-not (string-match-p "ONE00000" (buffer-string)))
|
||||||
|
(ebox-region-update "upper" :visibility 'hidden)
|
||||||
|
(should (string-match-p "ONE00000" (buffer-string)))
|
||||||
|
(should (string-match-p "TWO00000" (buffer-string)))
|
||||||
|
(should (string-match-p "Before" (buffer-string)))
|
||||||
|
(should (string-match-p "After" (buffer-string)))))))
|
||||||
|
|
||||||
|
(provide 'ebox-layer-publication-tests)
|
||||||
|
;;; ebox-layer-publication-tests.el ends here
|
||||||
236
tests/ebox-layer-style-tests.el
Normal file
236
tests/ebox-layer-style-tests.el
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
;;; ebox-layer-style-tests.el --- Layer style boundary tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ebox-style)
|
||||||
|
(require 'ebox)
|
||||||
|
|
||||||
|
(defconst ebox-layer-style-test--defaults
|
||||||
|
'(:position static :left (px 0) :top (lh 0)
|
||||||
|
:z-index 0 :layer local :anchor nil :placement bottom-start)
|
||||||
|
"Initial values of the public layer style properties.")
|
||||||
|
|
||||||
|
(defconst ebox-layer-style-test--values
|
||||||
|
'(:position absolute :left (ch -1.25) :top (lh -0.5)
|
||||||
|
:z-index -2 :layer root :anchor menu-button :placement top-end)
|
||||||
|
"Nondefault layer declarations covering every public layer property.")
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-schema-owns-geometry-facts ()
|
||||||
|
"Layer properties use box geometry projections without item participation."
|
||||||
|
(cl-loop for (name initial) on ebox-layer-style-test--defaults by #'cddr
|
||||||
|
for property = (ebox-style-property name)
|
||||||
|
do (should (equal (plist-get property :initial) initial))
|
||||||
|
do (should (equal (plist-get property :contexts)
|
||||||
|
'(box row column flex grid)))
|
||||||
|
do (should-not (plist-get property :inherits))
|
||||||
|
do (should (eq (plist-get property :signature) 'layout))
|
||||||
|
do (should (eq (ebox-style-dirty-kind name) 'geometry))
|
||||||
|
do (should (equal (plist-get property :impacts) '(geometry)))
|
||||||
|
do (should (equal (plist-get property :projections) '(layout)))
|
||||||
|
do (should (equal (plist-get property :engine-targets) (list name)))
|
||||||
|
do (should (eq (plist-get property :owner) 'ebox-box-frame)))
|
||||||
|
(dolist (name '(:layer :anchor :placement))
|
||||||
|
(should (eq (plist-get (ebox-style-property name) :namespace) 'ebox))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-accepts-existing-box-layouts-only ()
|
||||||
|
"Every box layout accepts layer facts, while Text rejects every property."
|
||||||
|
(dolist (tag '(box row column flex grid))
|
||||||
|
(let ((compiled (ebox-style-compile-form
|
||||||
|
tag ebox-layer-style-test--values)))
|
||||||
|
(cl-loop for (name value) on ebox-layer-style-test--values by #'cddr
|
||||||
|
do (should (equal (plist-get compiled (ebox-style-schema-id name))
|
||||||
|
value)))))
|
||||||
|
(cl-loop for (name value) on ebox-layer-style-test--defaults by #'cddr
|
||||||
|
do (should-error (ebox-style-compile-form 'text (list name value)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-enums-and-anchor-grammar ()
|
||||||
|
"Public enums, signed integer depth, and optional semantic anchors compile."
|
||||||
|
(dolist (case '((:position static relative absolute)
|
||||||
|
(:z-index -100 0 100)
|
||||||
|
(:layer local root)
|
||||||
|
(:anchor nil menu-button "menu-button")
|
||||||
|
(:placement bottom-start bottom-end top-start top-end)))
|
||||||
|
(dolist (value (cdr case))
|
||||||
|
(should (equal (ebox-style-compile-form 'box (list (car case) value))
|
||||||
|
(list (ebox-style-schema-id (car case)) value))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-rejects-invalid-static-declarations ()
|
||||||
|
"Invalid layer values signal at both DSL and stylesheet boundaries."
|
||||||
|
(dolist (case '((:position nil t fixed sticky normal "absolute" (absolute))
|
||||||
|
(:z-index nil t auto 1.0 -1.5 "2" (px 2) (lambda () 2))
|
||||||
|
(:layer nil t absolute global "root" (root))
|
||||||
|
(:anchor 1 1.5 (target) (:id target) [target])
|
||||||
|
(:placement nil t bottom center left "top-end" (top-end))))
|
||||||
|
(dolist (value (cdr case))
|
||||||
|
(ert-info ((format "%S rejects %S" (car case) value))
|
||||||
|
(should-error (ebox-style-compile-form 'box (list (car case) value)))
|
||||||
|
(should-error (ebox-style-compile-declarations
|
||||||
|
(list (car case) value) t))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-signed-offset-axis-matrix ()
|
||||||
|
"Signed offsets accept only typed lengths from their rendering axis."
|
||||||
|
(dolist (case '((:left inline) (:top block)))
|
||||||
|
(dolist (unit '(px ch vw lh vh %))
|
||||||
|
(dolist (amount '(-2 -0.25 0 0.25 2))
|
||||||
|
(let ((declarations (list (car case) (list unit amount))))
|
||||||
|
(ert-info ((format "%S" declarations))
|
||||||
|
(if (memq unit (cdr (assq (cadr case) ebox-size-axis-units)))
|
||||||
|
(should (ebox-style-compile-form 'box declarations))
|
||||||
|
(should-error (ebox-style-compile-form 'box declarations)
|
||||||
|
:type 'ebox-size-error)))))))
|
||||||
|
(dolist (name '(:left :top))
|
||||||
|
(dolist (value '(nil auto none 0 -2 0.25 (2) (px) (lh 1 2)
|
||||||
|
(em 1) (px "1") (lh -1.0e+INF) (px 0.0e+NaN)
|
||||||
|
(calc 2) (calc (/ (% 2) 0))))
|
||||||
|
(should-error (ebox-style-compile-form 'box (list name value))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-offset-expressions-preserve-axis-and-sign ()
|
||||||
|
"Expression operands keep axis validation even when they cancel out."
|
||||||
|
(dolist (case '((:left (calc (- (ch 1) (% 50))))
|
||||||
|
(:left (min (px -2) (vw -10)))
|
||||||
|
(:left (clamp (ch -3) (% -20) (px 4)))
|
||||||
|
(:top (calc (+ (lh -0.5) (vh 10))))
|
||||||
|
(:top (max (lh -2) (% -30)))
|
||||||
|
(:top (clamp (lh -3) (% -20) (vh 4)))))
|
||||||
|
(should (equal (ebox-style-compile-form 'box case)
|
||||||
|
(list (ebox-style-schema-id (car case)) (cadr case)))))
|
||||||
|
(dolist (case '((:left (calc (* 0 (lh -1))))
|
||||||
|
(:left (min (ch -1) (vh 0)))
|
||||||
|
(:top (calc (* 0 (px -1))))
|
||||||
|
(:top (max (lh -1) (vw 0)))))
|
||||||
|
(should-error (ebox-style-compile-form 'box case) :type 'ebox-size-error)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-defaults-do-not-inherit ()
|
||||||
|
"A child starts with layer defaults despite its parent's layer facts."
|
||||||
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||||
|
(subject (ecss-subject-create :type "box"))
|
||||||
|
(parent (ebox-style-compute-subject
|
||||||
|
subject (ebox-style-compile-form
|
||||||
|
'box ebox-layer-style-test--values)))
|
||||||
|
(child (ebox-style-compute-subject subject nil parent)))
|
||||||
|
(cl-loop for (name initial) on ebox-layer-style-test--defaults by #'cddr
|
||||||
|
do (should (equal (ebox-style-computed-value child name)
|
||||||
|
initial)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-projection-preserves-contextual-offsets ()
|
||||||
|
"Projection retains signed fractional lengths for final used placement."
|
||||||
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||||
|
(style (ebox-style-compute-subject
|
||||||
|
(ecss-subject-create :type "box")
|
||||||
|
(ebox-style-compile-form
|
||||||
|
'box ebox-layer-style-test--values)))
|
||||||
|
(snapshot (ebox-style--computed-snapshot style))
|
||||||
|
(projected (ebox-style--expand-engine-plist
|
||||||
|
(ebox-style--box-values style snapshot)))
|
||||||
|
(text (ebox-style--text-values style snapshot))
|
||||||
|
(item (ebox-style--context-values style 'item)))
|
||||||
|
(cl-loop for (name value) on ebox-layer-style-test--values by #'cddr
|
||||||
|
do (should (equal (plist-get projected name) value))
|
||||||
|
do (should-not (plist-member text name))
|
||||||
|
do (should-not (plist-member item name)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-changes-exclude-paint-only-proof ()
|
||||||
|
"A change to any layer declaration changes the layout signature."
|
||||||
|
(cl-loop for (name value) on ebox-layer-style-test--values by #'cddr
|
||||||
|
for old = (ebox-style-compile-form
|
||||||
|
'box (list name (plist-get
|
||||||
|
ebox-layer-style-test--defaults name)))
|
||||||
|
for new = (ebox-style-compile-form 'box (list name value))
|
||||||
|
do (should-not (ebox-style--paint-declarations-equivalent-p old new))
|
||||||
|
do (should-not (equal (ebox-style-signature old '(layout))
|
||||||
|
(ebox-style-signature new '(layout))))
|
||||||
|
do (should-not (ebox-style-signature new '(paint)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-runtime-removal-restores-defaults ()
|
||||||
|
"Removing or resetting declarations clears every old box layer fact."
|
||||||
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||||
|
(subject (ecss-subject-create :type "box"))
|
||||||
|
(positioned (ebox-style-compute-subject
|
||||||
|
subject (ebox-style-compile-form
|
||||||
|
'box ebox-layer-style-test--values))))
|
||||||
|
(dolist (tag '(box row column flex grid))
|
||||||
|
(dolist (declarations (list nil ebox-layer-style-test--defaults))
|
||||||
|
(let* ((node (ebox-canonical-input--single-root
|
||||||
|
(ebox-build (list tag "Unchanged")) "layer style test"))
|
||||||
|
(children (ebox-tree-node-children node))
|
||||||
|
(config (plist-get node :ebox-layout-config))
|
||||||
|
(reset (ebox-style-compute-subject
|
||||||
|
subject (ebox-style-compile-form 'box declarations))))
|
||||||
|
(ebox-style-apply-computed node positioned)
|
||||||
|
(ebox-style-apply-computed node reset)
|
||||||
|
(cl-loop for (name initial) on ebox-layer-style-test--defaults by #'cddr
|
||||||
|
do (should (equal (plist-get node name) initial)))
|
||||||
|
(should (equal (ebox-tree-node-children node) children))
|
||||||
|
(should (equal (plist-get node :ebox-layout-config) config)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-runtime-transfer-preserves-current-projection ()
|
||||||
|
"Source-stable transfers copy layer values and restore cleared defaults."
|
||||||
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||||
|
(subject (ecss-subject-create :type "box")))
|
||||||
|
(dolist (tag '(box row column flex grid))
|
||||||
|
(dolist (declarations (list ebox-layer-style-test--values nil))
|
||||||
|
(let* ((old (ebox-canonical-input--single-root
|
||||||
|
(ebox-build (list tag "Old")) "layer style test"))
|
||||||
|
(new (ebox-canonical-input--single-root
|
||||||
|
(ebox-build (list tag "New")) "layer style test"))
|
||||||
|
(children (ebox-tree-node-children new))
|
||||||
|
(style (ebox-style-compute-subject
|
||||||
|
subject (ebox-style-compile-form 'box declarations))))
|
||||||
|
;; Start both nodes with stale nondefaults, so copying a cleared
|
||||||
|
;; projection must overwrite them as well as retaining active facts.
|
||||||
|
(ebox-style-apply-computed
|
||||||
|
old (ebox-style-compute-subject
|
||||||
|
subject (ebox-style-compile-form
|
||||||
|
'box ebox-layer-style-test--values)))
|
||||||
|
(ebox-style-apply-computed old style)
|
||||||
|
(ebox-style-apply-computed
|
||||||
|
new (ebox-style-compute-subject
|
||||||
|
subject (ebox-style-compile-form
|
||||||
|
'box (if declarations
|
||||||
|
ebox-layer-style-test--defaults
|
||||||
|
ebox-layer-style-test--values))))
|
||||||
|
(ebox-style--transfer-computed-projection old new style)
|
||||||
|
(cl-loop for (name value)
|
||||||
|
on (or declarations ebox-layer-style-test--defaults) by #'cddr
|
||||||
|
do (should (equal (plist-get new name) value)))
|
||||||
|
(should (equal (ebox-tree-node-children new) children)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-stylesheet-updates-reset-runtime-projection ()
|
||||||
|
"Changing and removing a rule updates the same canonical node's fields."
|
||||||
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||||
|
(node (ebox-canonical-input--single-root
|
||||||
|
(ebox-build '(box :class "panel" "Panel")) "layer style test"))
|
||||||
|
(subject (ecss-subject-create :type "box" :classes '("panel"))))
|
||||||
|
(dolist (values (list ebox-layer-style-test--values
|
||||||
|
'(:position relative :left (px 3)
|
||||||
|
:top (lh 2) :z-index 10 :layer local :anchor nil
|
||||||
|
:placement bottom-end)
|
||||||
|
ebox-layer-style-test--defaults))
|
||||||
|
(ebox-style-add-rule ".panel" values)
|
||||||
|
(ebox-style-apply-computed
|
||||||
|
node (ebox-style-compute-subject subject nil))
|
||||||
|
(cl-loop for (name value) on values by #'cddr
|
||||||
|
do (should (equal (plist-get node name) value))))
|
||||||
|
(ebox-style-add-rule ".panel" ebox-layer-style-test--values)
|
||||||
|
(ebox-style-apply-computed node (ebox-style-compute-subject subject nil))
|
||||||
|
(setq ebox-style-stylesheet (ecss-stylesheet-create))
|
||||||
|
(ebox-style-apply-computed node (ebox-style-compute-subject subject nil))
|
||||||
|
(cl-loop for (name value) on ebox-layer-style-test--defaults by #'cddr
|
||||||
|
do (should (equal (plist-get node name) value)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-style-runtime-defaults-stay-off-text ()
|
||||||
|
"Applying a style with layer facts does not materialize them onto Text."
|
||||||
|
(let* ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||||
|
(node (ebox-canonical-input--single-root
|
||||||
|
(ebox-build '(text "Text")) "layer style test"))
|
||||||
|
(style (ebox-style-compute-subject
|
||||||
|
(ecss-subject-create :type "text")
|
||||||
|
(ebox-style-compile-form
|
||||||
|
'box ebox-layer-style-test--values))))
|
||||||
|
(ebox-style-apply-computed node style)
|
||||||
|
(cl-loop for (name _value) on ebox-layer-style-test--defaults by #'cddr
|
||||||
|
do (should-not (plist-member node name)))))
|
||||||
|
|
||||||
|
(provide 'ebox-layer-style-tests)
|
||||||
|
;;; ebox-layer-style-tests.el ends here
|
||||||
81
tests/ebox-layer-tests.el
Normal file
81
tests/ebox-layer-tests.el
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
;;; ebox-layer-tests.el --- Box composition contracts -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ebox)
|
||||||
|
|
||||||
|
(defun ebox-layer-test--render (form)
|
||||||
|
"Render FORM through the public canonical boundary."
|
||||||
|
(ebox-render (ebox-build form)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-minimal-opaque-overlap ()
|
||||||
|
(let ((output
|
||||||
|
(ebox-layer-test--render
|
||||||
|
'(box :width (ch 10) :height (lh 3)
|
||||||
|
(box "abcdefghij\nABCDEFGHIJ\n0123456789")
|
||||||
|
(box :position absolute :left (ch 2) :top (lh 1)
|
||||||
|
:width (ch 4) :height (lh 1) :background-color "red"
|
||||||
|
"TOP")))))
|
||||||
|
(should (equal (substring-no-properties output)
|
||||||
|
"abcdefghij\nABTOP GHIJ\n0123456789"))
|
||||||
|
(should (= (length (ebox-string-lines output)) 3))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-relative-reserves-flow-slot ()
|
||||||
|
(let ((lines
|
||||||
|
(ebox-string-lines
|
||||||
|
(ebox-layer-test--render
|
||||||
|
'(column :width (ch 10) :height (lh 3)
|
||||||
|
(box :position relative :left (ch 2) :top (lh 1)
|
||||||
|
:z-index 1 :width (ch 4) "MOVE")
|
||||||
|
(box "abcdefghij")
|
||||||
|
(box "0123456789"))))))
|
||||||
|
(should (= (length lines) 3))
|
||||||
|
(should-not (string-match-p "MOVE" (car lines)))
|
||||||
|
(should (equal (substring-no-properties (cadr lines)) "abMOVEghij"))
|
||||||
|
(should (equal (substring-no-properties (nth 2 lines)) "0123456789"))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-negative-z-stays-below-normal-content ()
|
||||||
|
(let ((output (ebox-layer-test--render
|
||||||
|
'(box :width (ch 6) :height (lh 1)
|
||||||
|
(box "BOTTOM")
|
||||||
|
(box :position absolute :width (ch 6) :z-index -1 "HIDDEN")))))
|
||||||
|
(should (equal (substring-no-properties output) "BOTTOM"))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-nested-isolation-orders-groups ()
|
||||||
|
(let ((output (ebox-layer-test--render
|
||||||
|
'(box :width (ch 6) :height (lh 1)
|
||||||
|
(box
|
||||||
|
(box "BOTTOM")
|
||||||
|
(box :position absolute :width (ch 6)
|
||||||
|
:z-index 100 "INSIDE"))
|
||||||
|
(box :position absolute :width (ch 6) :z-index 1 "FRONT!")))))
|
||||||
|
(should (equal (substring-no-properties output) "FRONT!"))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-root-projection-escapes-local-clipping ()
|
||||||
|
(let ((output (ebox-layer-test--render
|
||||||
|
'(box :width (ch 10) :height (lh 3)
|
||||||
|
(box :height (lh 1) :overflow hidden
|
||||||
|
(box :id trigger "BUTTON")
|
||||||
|
(box :position absolute :layer root :anchor trigger
|
||||||
|
:width (ch 4) :height (lh 1) "MENU"))
|
||||||
|
(box "abcdefghij\n0123456789")))))
|
||||||
|
(should (equal (substring-no-properties output)
|
||||||
|
"BUTTON \nMENUefghij\n0123456789"))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-missing-anchor-does-not-paint ()
|
||||||
|
(let ((output (ebox-layer-test--render
|
||||||
|
'(box :width (ch 6) :height (lh 1)
|
||||||
|
(box "BOTTOM")
|
||||||
|
(box :position absolute :anchor missing
|
||||||
|
:width (ch 6) "HIDDEN")))))
|
||||||
|
(should (equal (substring-no-properties output) "BOTTOM"))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-layer-hidden-upper-reveals-base ()
|
||||||
|
(let ((output (ebox-layer-test--render
|
||||||
|
'(box :width (ch 6) :height (lh 1)
|
||||||
|
(box "LATEST")
|
||||||
|
(box :position absolute :visibility hidden
|
||||||
|
:width (ch 6) "HIDDEN")))))
|
||||||
|
(should (equal (substring-no-properties output) "LATEST"))))
|
||||||
|
|
||||||
|
(provide 'ebox-layer-tests)
|
||||||
|
;;; ebox-layer-tests.el ends here
|
||||||
Loading…
Reference in New Issue
Block a user