Compare commits
11 Commits
perf-scrol
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8631531ebb | ||
|
|
40a6b36bcc | ||
|
|
834c05f18b | ||
|
|
6cd5a67a35 | ||
|
|
046b1cf87b | ||
|
|
cdefca440e | ||
|
|
65a24e2b69 | ||
|
|
dd7541e535 | ||
|
|
68996998af | ||
|
|
2e9a303424 | ||
|
|
81957acdf3 |
25
AGENTS.md
Normal file
25
AGENTS.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Ebox Playground guidance
|
||||
|
||||
Keep this package a readable collection of public Ebox DSL examples and a generic file runner. Use only public Ebox APIs in examples and runner code; keep test and diagnostic access separate.
|
||||
|
||||
Default authoring rule: use the shortest declaration that preserves the intended effect. Omit redundant defaults, place shared inheritable text styles on an existing common parent, and write only child differences. Do not add helper functions, macros, or wrappers merely to hide repeated property lists. Keep values explicit when the panel teaches that property or when they override another declaration. Backgrounds do not inherit, and `auto` and `stretch` are only interchangeable in specific layout contexts. See the [Ebox authoring rules](../ebox/docs/user/ebox-user-guide.en.md).
|
||||
|
||||
Before simplifying examples, preserve the current files as a comparison baseline and run the existing tests. Keep text, teaching coverage, layout behavior, effective colors, and the user's root viewport settings. Verify rendered output at multiple viewport sizes and inspect the existing GUI preview without changing its font, frame geometry, or application focus. Use `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs` for package validation. Extend existing test/evaluator helpers for repeated comparison work rather than writing competing scripts.
|
||||
|
||||
For before/after example comparisons, use `ebox-playground-compare-example-directories` from `scripts/ebox-playground-flex-resize-evaluator.el`; it accepts baseline/current example directories and optional viewport widths/file names. This is the shared entry point for future example simplification checks. See README for the batch invocation.
|
||||
|
||||
For scrolling performance across examples, use `ebox-playground-scroll-evaluator-run` from that same evaluator. Pass one `.ebox` path or a list, viewport width, sample count, and viewport height; it measures cached top/midpoint publication and verifies fresh-render parity while preserving existing GUI previews. Consult this entry point before adding another scroll probe, and extend it when the diagnostic coverage is missing. See README for the invocation and measurement boundaries.
|
||||
|
||||
For repeated interaction behavior or command performance, use `ebox-playground-interaction-evaluator-run` from `scripts/ebox-playground-flex-resize-evaluator.el`. Pass the `.ebox` file and visible control label, with optional key, sample count, width, and height. It performs native key lookup without repositioning after updates, reports command-loop timings/GC/publications and layout/projection work counters, and verifies committed-snapshot render parity. Read fragment call counts with their input-character totals to distinguish local slices from full-surface work. For native click dispatch in an already visible buffer, use the same file's `ebox-playground-interaction-evaluator-click` with buffer, label, and optional in-label offset; it resolves an Emacs event and calls the interactive command, not an OS mouse click. Consult and extend these entry points before adding another interaction probe; see README for inputs, cleanup, the between-sample deadline, and measurement limits.
|
||||
|
||||
Keep intentional spacer height explicit: use `(box :height (lh 1))` for a one-line separator, omitting width when ordinary block sizing already fills its parent. An otherwise empty Box with automatic height has zero content height.
|
||||
|
||||
For size declarations, consult the [shared Ebox unit-constraint table](../ebox/docs/user/ebox-user-guide.en.md#size-unit-constraints). Do not duplicate its matrix in a Playground-specific parser or guideline. Check the property's axis, every nested function branch, both axes after shorthand expansion, and the separate border-paint rule. Invalid combinations must fail at initial construction; examples must not depend on cross-axis conversion.
|
||||
|
||||
For size, unit, keyword, or sizing-function demonstration work, maintain `examples/size-reference.ebox` and run `make size-tests` (with `EMACS` set to the intended executable). Extend `tests/ebox-playground-size-tests.el` for coverage or numeric regressions instead of creating another size gallery or verification script.
|
||||
|
||||
Keep necessary business functions and variables in a same-basename `.el` file beside the `.ebox` layout. The generic runner loads that exact companion source, when present, before every preview evaluation, including `C-c C-c` and `ebox-playground-open-file`. Keep the `.ebox` focused on layout and its data references; do not add helpers merely to hide repeated property lists. Reload state according to ordinary Elisp definitions, and keep each companion lexically bound.
|
||||
|
||||
For native help, pointer, hover-style, or keymap demonstration work, maintain `examples/interaction-reference.el` and `examples/interaction-reference.ebox`, then run `make interaction-tests` (with `EMACS` set to the intended executable). Extend `tests/ebox-playground-interaction-tests.el` for capability, command, replacement, and removal regressions. Use `ebox-help-create` and `ebox-keymap-create` for zero-argument business callbacks and native help/command buffer context, and use `ebox-region-update` with semantic IDs in that buffer. These reusable adapters belong to Ebox; moving local adapters into the companion is not a substitute. Raw native callbacks and keymaps remain supported and own their event-context handling. Extend the generic runner tests for companion reload behavior rather than adding an example-specific loading mechanism.
|
||||
|
||||
For layer, overlap, positioning, or anchored-menu demonstrations, maintain `examples/layer-reference.ebox` and its same-basename companion, preserve `examples/layer-minimal.ebox` as the minimal fixture, and run `make layer-tests`. Extend `tests/ebox-playground-layer-tests.el` for actual native commands and committed-snapshot render parity instead of creating another demo runner or probe. Keep layout in the DSL, business state/actions in the companion, and one font size throughout positioned content. Use the shared interaction evaluator above for repeated layer-command timing. Each parent creates its local stacking group automatically; use `:layer root` on a nested absolute node when root projection is required, with semantic `:anchor` and `:placement` declarations for anchored menus.
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## Source of truth
|
||||
- Status: Active
|
||||
- Last refreshed: 2026-08-05
|
||||
- Last refreshed: 2026-09-08
|
||||
- Primary product surfaces: The `.ebox` files under `examples/`, the buffer opened by `ebox-playground-open`, and the generic file mode.
|
||||
- Evidence reviewed: `ebox-playground.el`, the `.ebox` fixtures under `examples/`, `tests/ebox-playground-tests.el`, `README.md`, `README.zh-CN.md`, and rendered gallery/reference previews.
|
||||
|
||||
@ -44,7 +44,9 @@
|
||||
- Author forms to reuse: String, `text`, `box`, `row`, `column`, `flex`, and `grid`. Empty `box` nodes express spacing directly without another public form.
|
||||
- New/changed components: Header band, property section bands, track-function cards, implicit-track/flow cards, gap/placement cards, alignment cards, and public-contract footer.
|
||||
- Variants and states: Static semantic color families only.
|
||||
- Token/component ownership: Palette values and example composition live in `.ebox` fixtures; the runner owns only parsing and rendering. The migrated references remain data files, so adding a layout does not add a fixture-specific Elisp branch.
|
||||
- Token/component ownership: Palette values and example composition live in `.ebox` fixtures; the runner reads and evaluates one ordinary Elisp expression with lexical bindings, then passes the resulting DSL data to public `ebox-build` and renders it. Adding a layout does not add a fixture-specific runner branch.
|
||||
- Source contract: A static layout quotes the whole list; dynamic layouts use normal Elisp such as `let`, backquote, comma, and comma splicing. The expression is identical to the argument of `ebox-build` in an `.el` file. The runner does not evaluate properties separately or auto-detect the old unquoted structural format.
|
||||
- Size contract: Use Ebox's explicit `(unit number)` values (`px`, `%`, `vw`, `vh`, `ch`, `lh`) and `calc`/`min`/`max`/`clamp` data. Bare `fit-content` is a keyword, never a function. Ebox owns resolution and vertical line quantization; the Playground adds no unit parser or alternate semantics.
|
||||
|
||||
## Accessibility
|
||||
- Target standard: High contrast and readable labels in ordinary GUI Emacs themes.
|
||||
@ -55,7 +57,7 @@
|
||||
|
||||
## Responsive behavior
|
||||
- Supported breakpoints/devices: GUI Emacs windows from compact split previews to fullscreen desktop widths.
|
||||
- Layout adaptations: Standalone gallery entry points default to a 720 px content canvas; split-window `.ebox` previews bind `(viewport)` to the preview pane's display-safe width so viewport-based sections fit the active pane.
|
||||
- Layout adaptations: Standalone gallery entry points default to a 720 px content canvas; split-window `.ebox` previews resolve `(vw 100)` against the preview pane's display-safe width and `(vh 100)` against its body height so viewport-based sections fit the active pane.
|
||||
- Touch/hover differences: None.
|
||||
|
||||
## Interaction states
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## 真相来源
|
||||
- 状态:有效
|
||||
- 最近更新:2026-08-05
|
||||
- 最近更新:2026-09-08
|
||||
- 主要产品界面:`examples/` 下的 `.ebox` 文件、`ebox-playground-open` 打开的缓冲区,以及通用文件模式。
|
||||
- 已审阅证据:`ebox-playground.el`、`examples/` 下的 `.ebox` fixture、`tests/ebox-playground-tests.el`、`README.md`、`README.zh-CN.md` 以及画廊/参考文件的实际渲染。
|
||||
|
||||
@ -44,7 +44,9 @@
|
||||
- 复用的作者 form:String、`text`、`box`、`row`、`column`、`flex` 和 `grid`。空 `box` 直接表达间距,不再引入另一个公共 form。
|
||||
- 新增/调整的组件:标题带、能力分区标题带、固定/分数轨道卡片、放置卡片和公开契约页脚。
|
||||
- 变体与状态:仅使用静态语义色组。
|
||||
- Token/组件归属:配色值和示例组合放在 `.ebox` fixture 中;运行器只负责解析和渲染。迁移来的参考仍然是数据文件,新增布局不需要在 Elisp 中增加针对 fixture 的分支。
|
||||
- Token/组件归属:配色值和示例组合放在 `.ebox` fixture 中;运行器读取唯一一个普通 Elisp 表达式,以词法绑定求值,将结果作为 DSL 数据传给公开 `ebox-build` 并渲染。新增布局不需要在运行器中增加针对 fixture 的分支。
|
||||
- 源文件契约:静态布局对整个列表加 quote;动态布局使用普通 Elisp 的 `let`、反引号、逗号和逗号展开。该表达式与 `.el` 中 `ebox-build` 的参数一致。运行器不再单独求值属性,也不自动识别旧的裸结构格式。
|
||||
- 尺寸契约:使用 Ebox 的显式 `(单位 数值)`(`px`、`%`、`vw`、`vh`、`ch`、`lh`)和 `calc`/`min`/`max`/`clamp` 数据。裸 `fit-content` 是关键词,不是函数。尺寸解析和纵向行量化由 Ebox 负责,Playground 不增加单位解析器或另一套语义。
|
||||
|
||||
## 无障碍
|
||||
- 目标标准:在常见 GUI Emacs 主题下保持高对比度和标签可读性。
|
||||
@ -55,7 +57,7 @@
|
||||
|
||||
## 响应式行为
|
||||
- 支持的断点/设备:正文宽度至少为 760 px 的 GUI Emacs 窗口,从紧凑窗口到全屏桌面。
|
||||
- 布局适配:画廊统一拥有一个固定的 720 px 内容画布;嵌套 Grid 和 Flex 通过公开 Ebox 布局行为划分该宽度,而不是各自读取视口。
|
||||
- 布局适配:独立画廊默认使用 720 px 内容画布;分屏 `.ebox` 预览将 `(vw 100)` 解析为预览窗格的显示安全宽度,将 `(vh 100)` 解析为正文区域高度,让视口相对区块适应预览窗格。
|
||||
- 触控/悬停差异:无。
|
||||
|
||||
## 交互状态
|
||||
|
||||
61
Makefile
61
Makefile
@ -2,32 +2,75 @@ EMACS ?= emacs
|
||||
ECSS_DIR ?= ../ecss
|
||||
TP_DIR ?= ../tp
|
||||
LOAD_PATH = -L . -L ../ebox -L $(ECSS_DIR) -L $(TP_DIR)
|
||||
EBOX_NATIVE_RELEASE_DIR = $(shell $(EMACS) -Q --batch $(LOAD_PATH) \
|
||||
--eval '(setq native-comp-jit-compilation nil)' \
|
||||
-l ../ebox/ebox-native-reflow.el \
|
||||
--eval '(princ (expand-file-name (format "../ebox/native/target/%s/release/" (ebox-native-reflow--rust-target))))')
|
||||
|
||||
.PHONY: all compile test check checkdoc load performance-evaluator clean
|
||||
.PHONY: all compile test size-tests interaction-tests layer-tests check checkdoc load performance performance-prepare \
|
||||
performance-evaluator clean
|
||||
|
||||
all: check
|
||||
|
||||
compile:
|
||||
rm -f *.elc tests/*.elc
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \
|
||||
rm -f *.elc tests/*.elc examples/*.elc
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil byte-compile-error-on-warn t)' \
|
||||
--eval '(load-file "ebox-playground.el")' \
|
||||
--eval '(byte-compile-file "ebox-playground.el")'
|
||||
--eval '(byte-compile-file "ebox-playground.el")' \
|
||||
--eval '(dolist (file (directory-files "examples" t)) (when (string-suffix-p ".el" file) (unless (byte-compile-file file) (error "Companion compilation failed: %s" file))))' \
|
||||
--eval '(byte-compile-file "scripts/ebox-playground-flex-resize-evaluator.el")'
|
||||
|
||||
test: compile
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(setq load-prefer-newer t)' \
|
||||
-l tests/ebox-playground-tests.el -f ert-run-tests-batch-and-exit
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
||||
-l tests/ebox-playground-tests.el -l tests/ebox-playground-size-tests.el \
|
||||
-l tests/ebox-playground-interaction-tests.el -l tests/ebox-playground-layer-tests.el \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
|
||||
layer-tests: compile
|
||||
$(MAKE) -C ../ebox layer-tests EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
||||
-l tests/ebox-playground-layer-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
interaction-tests: compile
|
||||
$(MAKE) -C ../ebox interaction-tests EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
||||
-l tests/ebox-playground-interaction-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
size-tests: compile
|
||||
$(MAKE) -C ../ebox size-tests EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
||||
-l tests/ebox-playground-size-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
load: compile
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) --eval '(require (quote ebox-playground))' \
|
||||
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||
--eval '(setq native-comp-jit-compilation nil)' \
|
||||
--eval '(require (quote ebox-playground))' \
|
||||
--eval '(princ "ebox-playground load OK\n")'
|
||||
|
||||
performance:
|
||||
$(MAKE) performance-prepare EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
|
||||
$(MAKE) performance-evaluator EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
|
||||
|
||||
performance-prepare:
|
||||
$(MAKE) -C $(ECSS_DIR) compile EMACS="$(EMACS)"
|
||||
$(MAKE) -C $(TP_DIR) compile EMACS="$(EMACS)"
|
||||
$(MAKE) -C ../ebox compile EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
|
||||
$(MAKE) compile EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
|
||||
$(MAKE) -C ../ebox native-build EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
|
||||
|
||||
performance-evaluator:
|
||||
EBOX_NATIVE_REFLOW_MODULE_PATH="$(EBOX_NATIVE_RELEASE_DIR)" \
|
||||
EMACS="$(EMACS)" ./scripts/flex-resize-evaluator.sh
|
||||
|
||||
checkdoc:
|
||||
$(EMACS) -Q --batch --eval '(progn (require (quote checkdoc)) (dolist (file (directory-files "." t)) (when (string-suffix-p ".el" file) (checkdoc-file file))))'
|
||||
$(EMACS) -Q --batch --eval '(progn (require (quote checkdoc)) (dolist (directory (quote ("." "examples"))) (dolist (file (directory-files directory t)) (when (string-suffix-p ".el" file) (checkdoc-file file)))))'
|
||||
|
||||
check: checkdoc compile test
|
||||
|
||||
clean:
|
||||
rm -f *.elc tests/*.elc
|
||||
rm -f *.elc tests/*.elc examples/*.elc
|
||||
|
||||
339
README.md
339
README.md
@ -1,24 +1,344 @@
|
||||
# ebox-playground
|
||||
|
||||
`ebox-playground` is the independent, generic file runner for Ebox DSL examples. It uses only public Ebox APIs; concrete layouts live in readable `.ebox` fixtures under `examples/`. The migrated low-level fixtures include the original Basic, Comprehensive, Flex, and Responsive references, while `grid-reference.ebox` provides a complete public Grid property/value gallery with the same restrained terracotta, sage, blue, violet, ochre, and teal visual language.
|
||||
`ebox-playground` is the independent, generic file runner for Ebox DSL examples. It uses only public Ebox APIs; concrete layouts live in readable `.ebox` fixtures under `examples/`. `flex-reference.ebox`, `grid-reference.ebox`, and `size-reference.ebox` demonstrate layout and sizing with a restrained terracotta, sage, blue, violet, ochre, and teal visual language. `interaction-reference.ebox` demonstrates native Ebox interaction and retained updates. `layer-reference.ebox` combines retained overlapping panels with a native anchored menu.
|
||||
|
||||
ECSS 0.1.0 and TP 1.0.0 are independent packages and may be installed in either order. Install both before Ebox 2.0.0, then install this package. `ebox-playground` never reaches into those packages' private APIs.
|
||||
ECSS 0.1.0 and TP 1.0.1 are independent packages and may be installed in either order. Install both before Ebox 2.0.1, then install Ebox Playground 0.1.1. `ebox-playground` never reaches into those packages' private APIs.
|
||||
|
||||
Standalone gallery entry points default to a definite 720 px canvas so nested examples have stable space in batch renders and one-window demos. Split-window `.ebox` previews instead bind `(viewport)` to the preview window's display-safe width, so viewport-based separators and sections fit the right-hand pane. `etaf-playground` remains the separate higher-level Component and Runtime showcase.
|
||||
Standalone gallery entry points default to a definite 720 px canvas so nested examples have stable space in batch renders and one-window demos. Split-window `.ebox` previews instead resolve `(vw 100)` to the preview window's display-safe width. Section separators use their containing Column's available width, so they also fit a narrower root. `etaf-playground` remains the separate higher-level Component and Runtime showcase.
|
||||
|
||||
Standalone and batch fixture renders use the same compact 720 px default viewport unless the caller dynamically binds `ebox-viewport-width`; viewport-based references therefore stay inside the example canvas. When `C-c C-c` renders an `.ebox` source buffer, the command keeps the source on the left, opens the preview on the right, and binds `(viewport)` to Ebox's shared display-safe window width. Ebox's single serialized viewport controller publishes every window change immediately through its retained path; the Playground owns no second resize hook or timer.
|
||||
Standalone and batch fixture renders use the same compact 720 px default viewport unless the caller dynamically binds `ebox-viewport-width`; viewport-based references therefore stay inside the example canvas. When `C-c C-c` renders an `.ebox` source buffer, the command keeps the source on the left, opens the preview on the right, and resolves `(vw 100)` to Ebox's shared display-safe window width. Ebox's single serialized viewport controller publishes every window change immediately through its retained path; the Playground owns no second resize hook or timer.
|
||||
|
||||
```elisp
|
||||
(require 'ebox-playground)
|
||||
(ebox-playground-open)
|
||||
```
|
||||
|
||||
The package also owns the `.ebox` file boundary. Visiting an Ebox DSL file selects `ebox-dsl-mode`; its header line shows `C-c C-c Render preview`. Press `C-c C-c` to build the single DSL form, render it through the public Ebox API, and automatically display the source and preview side by side.
|
||||
Open `examples/size-reference.ebox` and press `C-c C-c` for **Ebox Size
|
||||
Reference**. It demonstrates the six length units on their permitted axes, the supported keyword values
|
||||
for all six size constraints, `calc/min/max/clamp`, and combinations with fonts,
|
||||
spacing, Flex, and Grid. Resize the preview to compare percentages with viewport
|
||||
units and see fluid sizes change. Precision and buffer-renderer limitations are
|
||||
stated in the example rather than presented as browser-equivalent behavior.
|
||||
Constrained long-text samples separate intrinsic widths; Row samples separate
|
||||
`auto` from `stretch`. Height content keywords are explicitly marked equivalent
|
||||
in the current renderer. Shared low/middle/high inputs show how `min`, `max`,
|
||||
and `clamp` respond at their boundaries.
|
||||
Solid rectangles mark the measured Box's full extent, including empty space;
|
||||
pale rulers show available parent width, while white cards contain the labels.
|
||||
|
||||
Run `make size-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs` to check
|
||||
the core size contracts and the actual example declarations, known arithmetic
|
||||
results, painted sizing differences, complete rendering, and retained viewport updates. `make check` also
|
||||
includes the example's regression tests.
|
||||
|
||||
## ORBIT spatial desktop and layer lab
|
||||
|
||||
`examples/layer-minimal.ebox` is the smallest static overlap example. The complete
|
||||
lab opens with **ORBIT / CREATIVE OS**, a dark spatial desktop with violet,
|
||||
ice-blue, and acid-green cards, offset shadow slabs, nested geometric artwork,
|
||||
and floating badges. It uses the same generic runner and a companion for
|
||||
application state:
|
||||
|
||||
```elisp
|
||||
(ebox-playground-open-file
|
||||
(expand-file-name "examples/layer-reference.ebox"
|
||||
ebox-playground-directory))
|
||||
```
|
||||
|
||||
Visit the `.ebox` file and press `C-c C-c` for the source/preview split. Every
|
||||
control supports click, `RET`, and `SPC`, with native help and hover paint.
|
||||
All text uses one font size.
|
||||
|
||||
- **EXPLODE STACK** spreads the three cards apart and returns them to their
|
||||
original positions without moving the following sections.
|
||||
- **CYCLE FOCUS** brings the studio, task card, and player to the front in turn.
|
||||
- **SWITCH MOOD** changes the card palette between lunar violet/ice/lime and
|
||||
solar pink/apricot/mint. Each preview has independent state.
|
||||
- **UPDATE COVERED NOTE** changes a note fully covered by Panel A. Update it
|
||||
twice, then **TOGGLE PANEL A** to reveal revision 02. The revealed note has
|
||||
its own native action and live help; covering it never removes its logical
|
||||
data or bindings.
|
||||
- **MOVE PANEL A** changes `:left` and `:top` without adding flow rows.
|
||||
**RAISE / LOWER A** changes `:z-index` between 2 and 4 while Panel B stays at
|
||||
3. Opaque panel content and padding replace the lower layer's visible paint.
|
||||
- The relative marker reserves its original flow row and paints one row lower.
|
||||
Each parent forms a local stacking group automatically; no isolation
|
||||
declaration is needed.
|
||||
- **OPEN / CLOSE ROOT MENU** shows a menu whose logical owner clips to one
|
||||
row. The nested absolute menu declares `:layer root` and anchors to the
|
||||
trigger's semantic ID, so it can paint beyond that local clip.
|
||||
**CYCLE PLACEMENT** visits `bottom-start`, `bottom-end`, `top-start`, and
|
||||
`top-end`; **MOVE ANCHOR** moves the trigger and its open menu together.
|
||||
Choosing sage or amber updates the result and closes the menu.
|
||||
|
||||
`layer-reference.ebox` contains layout data and state references;
|
||||
`layer-reference.el` owns state and business actions. It uses only public
|
||||
`ebox-keymap-create`, `ebox-help-create`, and `ebox-region-update` APIs.
|
||||
Offsets use inline `ch` and block `lh` units. The text renderer composes whole
|
||||
rows, and positioned content must share its host's line height.
|
||||
|
||||
Run the batch checks without opening GUI windows:
|
||||
|
||||
```sh
|
||||
make layer-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||
```
|
||||
|
||||
This target runs core layer contracts and the actual demo's native commands,
|
||||
covered updates, movement, stacking, all four anchor placements, cross-buffer
|
||||
mouse dispatch, and independent previews. After each command, the tests compare
|
||||
the mounted output with a fresh render of the committed snapshot, including
|
||||
paint and native capabilities. `make check` includes the Playground tests.
|
||||
For repeated command timing, use the existing interaction evaluator with
|
||||
`"examples/layer-reference.ebox"` and a toolbar label such as
|
||||
`"UPDATE COVERED NOTE"`; see the invocation below.
|
||||
|
||||
<a id="native-interaction-lab"></a>
|
||||
## Native interaction lab
|
||||
|
||||
Open the interaction reference with the existing file runner:
|
||||
|
||||
```elisp
|
||||
(require 'ebox-playground)
|
||||
(ebox-playground-open-file
|
||||
(expand-file-name "examples/interaction-reference.ebox"
|
||||
ebox-playground-directory))
|
||||
```
|
||||
|
||||
You can also visit `examples/interaction-reference.ebox` and press `C-c C-c`
|
||||
for a source/preview split. **Ebox Native Interaction Lab** first separates
|
||||
help text and pointer shapes from hover paint, then combines `:help-echo`,
|
||||
`:pointer`, `:hover-style`, and `:keymap` in a live counter and event log.
|
||||
|
||||
- Hover the bordered cards, including their padding, to compare static help,
|
||||
dynamic help, native pointers, and color/text-decoration changes.
|
||||
- Click **ADD / [+]** to add or the separate **[-]** Text to subtract. Move
|
||||
point into an action to use `RET` / `SPC`, or use its `+` / `-` shortcuts.
|
||||
**SWITCH STEP 1 / 5** replaces its keymap with commands for the chosen step.
|
||||
- **SWITCH HOVER PALETTE** replaces hover paint. **REMOVE / RESTORE** clears
|
||||
all four capabilities with explicit `nil`, then restores them. The Text
|
||||
reset action resets the count.
|
||||
- **UPDATE STATUS GROUP** updates the three `.demo-status` Boxes together
|
||||
through one public `ebox-update-selector` call.
|
||||
- The nested surface demonstrates parent coverage, a Text child with its own
|
||||
commands, and an explicit-`nil` child that blocks enclosing capabilities.
|
||||
The event log records which command ran.
|
||||
|
||||
Box help, pointer, and keymap include content, padding, and border, excluding
|
||||
that Box's margin and structural newlines. A hover owner's text and padding
|
||||
share one native `mouse-face` within a rendered line; unspecified attributes
|
||||
use that owner's base style, including when child text normally differs.
|
||||
Physical left/right borders are excluded from hover; horizontal border strokes
|
||||
keep their paint. Native pointer appearance and help display depend on Emacs
|
||||
settings and the window system. See the
|
||||
[public capability contract](../ebox/docs/user/ebox-api-reference.en.md#native-node-capabilities).
|
||||
|
||||
`interaction-reference.el` owns the business functions and state;
|
||||
`interaction-reference.ebox` owns the layout. The runner reloads the matching
|
||||
`.el` before each preview. Ebox's `ebox-help-create` supplies the native help
|
||||
adapter; `ebox-keymap-create` binds
|
||||
zero-argument callbacks and updates named regions with
|
||||
`ebox-region-update`. Mouse callbacks automatically run in the event window's
|
||||
buffer. Ebox adds no application state or focus navigation system; use ordinary
|
||||
point motion for keyboard bindings. Keymaps are snapshotted, so publishing new
|
||||
bindings requires replacing `:keymap`. Each evaluation creates a fresh demo.
|
||||
|
||||
Run the interaction checks from this directory:
|
||||
|
||||
```sh
|
||||
make interaction-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||
```
|
||||
|
||||
The target verifies the core interaction contracts and this fixture's rendered
|
||||
capabilities, command effects, replacements, and removal/restoration.
|
||||
`make check` also includes the Playground interaction regression tests.
|
||||
|
||||
For repeated-key behavior and command timing, use the shared interaction
|
||||
evaluator:
|
||||
|
||||
```sh
|
||||
emacs -Q --batch -L . -L ../ebox -L ../ecss -L ../tp \
|
||||
-l scripts/ebox-playground-flex-resize-evaluator.el \
|
||||
--eval '(pp (ebox-playground-interaction-evaluator-run "examples/interaction-reference.ebox" "ADD " "RET" 30 720 200))'
|
||||
```
|
||||
|
||||
The signature is `(FILE LABEL &optional KEY STEPS WIDTH HEIGHT)`. It opens a
|
||||
fresh temporary preview, finds the visible label once, and repeats native key
|
||||
lookup at the resulting point without repositioning after updates. Losing the
|
||||
node's binding fails the run. `KEY` accepts a key-description string or event
|
||||
vector; defaults are `RET`, 30 commands, 720 px width, and 200 lines of height.
|
||||
Sample count and dimensions must be positive integers.
|
||||
|
||||
The returned plist reports mean/p95/max command milliseconds, GC count/time,
|
||||
allocated cons cells (`:cons-cells`, to track allocation pressure),
|
||||
publication count, final plain text, and parity with an independent render of
|
||||
the committed snapshot. Work counters `:layout-calls`, `:surface-plans`,
|
||||
`:fragment-scans`, and `:node-registrations` count calls to layout rendering,
|
||||
surface planning, fragment extraction (including local slices), and node-tree
|
||||
registration. `:fragment-characters` sums the characters passed to fragment
|
||||
extraction. Read the call counts alongside this character total to distinguish
|
||||
local work from full-surface scanning; call counts are not affected-node counts.
|
||||
A parity mismatch fails.
|
||||
|
||||
Timing includes synchronous command execution, publication, and GC. Timings,
|
||||
GC, publication counts, and work counters cover only the command loop;
|
||||
initial mounting and the final parity render are excluded. The evaluator
|
||||
excludes GUI redisplay, OS key-repeat
|
||||
latency, and idle prewarming. The sampling loop checks a 30-second deadline
|
||||
between commands. Temporary preview and owned timer cleanup runs on success,
|
||||
error, timeout, or interruption, preserving existing previews and windows.
|
||||
Reuse this entry point for interaction regressions instead of ad hoc key-repeat
|
||||
timing scripts.
|
||||
|
||||
For native click dispatch in an already visible preview, load the same evaluator
|
||||
and call `(ebox-playground-interaction-evaluator-click BUFFER LABEL &optional OFFSET)`.
|
||||
`BUFFER` is a live buffer or its name; the first matching label must be visible
|
||||
in a displaying window. `OFFSET` is a zero-based integer within the label and
|
||||
defaults to zero. For example, evaluate from the preview buffer:
|
||||
|
||||
```elisp
|
||||
(load (expand-file-name "scripts/ebox-playground-flex-resize-evaluator.el"
|
||||
ebox-playground-directory) nil t t)
|
||||
(ebox-playground-interaction-evaluator-click (current-buffer) "ADD " 0)
|
||||
(ebox-playground-interaction-evaluator-click (current-buffer) "[-]" 1)
|
||||
```
|
||||
|
||||
This constructs an Emacs mouse event, resolves the native binding, and uses
|
||||
`call-interactively`; it does not inject an OS mouse click. The helper does not
|
||||
select a window or move point itself. It returns the clicked position; check
|
||||
the counter or event log separately to verify the business result.
|
||||
|
||||
## Author and render `.ebox` files
|
||||
|
||||
The package also owns the `.ebox` file boundary. Visiting an Ebox DSL file selects `ebox-dsl-mode`; its header line shows `C-c C-c Render preview`. Press `C-c C-c` to evaluate the file's single Elisp expression, build its resulting layout data through `ebox-build`, and automatically display the source and preview side by side.
|
||||
|
||||
```elisp
|
||||
(add-to-list 'auto-mode-alist '("\\.ebox\\'" . ebox-dsl-mode))
|
||||
```
|
||||
|
||||
Write the same expression in `.ebox` that you would pass as the argument to
|
||||
`(ebox-build ...)` in Elisp. Quote a whole static layout; its list and symbol
|
||||
properties do not need additional quotes:
|
||||
|
||||
```elisp
|
||||
'(column :padding ((lh 1) (ch 2))
|
||||
:cross-align center
|
||||
(box :width (px 240) "Hello"))
|
||||
```
|
||||
|
||||
Keep necessary business functions and variables in an optional same-basename
|
||||
`.el` file beside the `.ebox`. Before each preview evaluation, the runner loads
|
||||
that exact `.el` source if it exists, then evaluates the `.ebox` expression.
|
||||
This applies to `C-c C-c`, `ebox-playground-open-file`, and file rendering;
|
||||
saved `.el` edits are loaded on the next preview without `require` caching or
|
||||
selecting stale bytecode. A missing companion is allowed; a companion load error
|
||||
stops evaluation. Ordinary Elisp variable definitions control state retention
|
||||
or reset on reload.
|
||||
|
||||
For example, put the data and action in `notes.el`:
|
||||
|
||||
```elisp
|
||||
;;; notes.el --- Notes example behavior -*- lexical-binding: t; -*-
|
||||
(defvar notes-body "A long article.")
|
||||
|
||||
(defun notes-help ()
|
||||
"Return business help for the article."
|
||||
(format "Article: %s" notes-body))
|
||||
|
||||
(defun notes-activate ()
|
||||
"Mark the article in the current Ebox buffer."
|
||||
(ebox-region-update "article" :color "#166534"))
|
||||
```
|
||||
|
||||
Keep `notes.ebox` focused on the layout:
|
||||
|
||||
```elisp
|
||||
`(column :padding ((lh 1) (ch 2))
|
||||
(box :id "article" :width (px 480)
|
||||
:help-echo ,(ebox-help-create #'notes-help)
|
||||
:pointer hand
|
||||
:keymap ,(ebox-keymap-create :activate #'notes-activate)
|
||||
,notes-body))
|
||||
```
|
||||
|
||||
`ebox-help-create` adapts a zero-argument business function returning a string
|
||||
or `nil` to native help with the hovered buffer as context. `ebox-keymap-create`
|
||||
handles `RET`, `[return]`, `SPC`, and `[mouse-1]` through
|
||||
`:activate`. Its optional `:bindings` alist pairs key-description strings or
|
||||
event vectors with zero-argument callbacks. Raw native keymaps are also accepted.
|
||||
See the [public helper contract](../ebox/docs/user/ebox-api-reference.en.md#native-node-capabilities).
|
||||
|
||||
The runner evaluates one `.ebox` expression with lexical bindings on each
|
||||
render. Backquote preserves layout data, `,` inserts a value, and `,@` inserts a
|
||||
list of children. Properties and children in the result are data; the runner
|
||||
does not evaluate them separately. Keep business definitions in the companion;
|
||||
use Ebox's public adapters for native help and command plumbing, and do not add
|
||||
helpers merely to hide repeated DSL property lists.
|
||||
|
||||
Prefer concise DSL: omit redundant defaults, put common inheritable text
|
||||
styles on an existing parent, and write child overrides only. Keep explicit
|
||||
values in panels that teach those properties. The [authoring rules](../ebox/docs/user/ebox-user-guide.en.md#prefer-defaults-and-inheritance)
|
||||
explain which properties inherit. Empty Boxes have zero automatic content
|
||||
height; give one-line separators an explicit `:height (lh 1)`.
|
||||
|
||||
For example simplifications, compare a saved baseline directory with the
|
||||
current `examples` directory using the existing evaluator:
|
||||
|
||||
```sh
|
||||
emacs -Q --batch -L . -L ../ebox -L ../ecss -L ../tp \
|
||||
-l scripts/ebox-playground-flex-resize-evaluator.el \
|
||||
--eval '(ebox-playground-compare-example-directories "/path/to/baseline" "examples")'
|
||||
```
|
||||
|
||||
The optional third and fourth arguments select viewport widths and file names.
|
||||
Defaults cover both references at 720 and 1000 px, including their complete
|
||||
documents. The comparison checks text, effective faces, display and interaction
|
||||
properties, and line widths; a mismatch fails the batch command. It creates no
|
||||
files or GUI windows and supersedes one-off comparison probes for this task.
|
||||
|
||||
For scrolling regressions across examples, use the same evaluator's generic
|
||||
scroll entry point:
|
||||
|
||||
```sh
|
||||
emacs -Q --batch -L . -L ../ebox -L ../ecss -L ../tp \
|
||||
-l scripts/ebox-playground-flex-resize-evaluator.el \
|
||||
--eval '(ebox-playground-scroll-evaluator-run (directory-files "examples" t "\\.ebox$") 721 20 45)'
|
||||
```
|
||||
|
||||
The arguments are one file path or a list of paths, viewport width in pixels,
|
||||
sample count, and viewport height in lines. Each file needs a scrollable root.
|
||||
Content is fully cached before alternating one-line motions at the top and
|
||||
midpoint. Results report mean/p95/max milliseconds, full root renders, node style
|
||||
visits, style computations, and fresh-render parity; incomplete motion or changed
|
||||
output fails the run. The independent render comparison ignores runtime identity
|
||||
and zero-pixel spacers beside identical font faces; height carriers are retained.
|
||||
This measures retained TP scroll publication, excluding
|
||||
native window scrolling, idle prewarming, and redisplay. To compare GUI font
|
||||
metrics, load this evaluator and call the same function in the existing GUI
|
||||
Emacs. It uses undisplayed temporary buffers, preserves live previews and windows,
|
||||
and cleans its mounts, advice, and timers even on errors or interruption. This
|
||||
entry point supersedes temporary per-example scroll timing probes.
|
||||
|
||||
Size data uses `(unit number)` with `px`, `%`, `vw`, `vh`, `ch`, and `lh`, plus
|
||||
`calc`, `min`, `max`, and `clamp`. The [shared unit-constraint
|
||||
table](../ebox/docs/user/ebox-user-guide.en.md#size-unit-constraints) defines which
|
||||
units each property accepts; the axes cannot be mixed, including in nested
|
||||
function branches. Ebox rejects invalid combinations when it builds the tree.
|
||||
For example, the preview can stay one line
|
||||
shorter than the viewport with `:height (calc (- (vh 100) (lh 1)))`; the
|
||||
expression is resolved by Ebox on each layout. `fit-content` stays a bare
|
||||
keyword and does not take an argument. Border thickness has its own paint-unit
|
||||
rule in the same table. Ebox's buffer backend quantizes
|
||||
vertical content to complete lines. See the [Ebox sizing
|
||||
contract](../ebox/docs/user/ebox-user-guide.en.md#4-use-geometry-and-paint-properties).
|
||||
|
||||
To migrate an old unquoted `.ebox` layout, quote the whole static list and
|
||||
remove the quotes around its list and symbol properties, for example
|
||||
`:padding '(1 2)` becomes `:padding ((lh 1) (ch 2))`. For dynamic values, use
|
||||
backquote and an explicit comma: a computed pixel width becomes
|
||||
`:width (px ,(+ 200 40))`. Bare lengths, singleton pixel lists, and the old
|
||||
viewport keywords must also be migrated. The runner does not auto-detect the
|
||||
legacy format.
|
||||
|
||||
The reference fixtures live directly under `examples/`. For example:
|
||||
|
||||
```elisp
|
||||
@ -28,3 +348,12 @@ The reference fixtures live directly under `examples/`. For example:
|
||||
```
|
||||
|
||||
Run `make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs` from this directory.
|
||||
|
||||
Run `make performance` for the committed Flex resize gate. It prepares ECSS,
|
||||
TP, Ebox, Playground bytecode, and the Rust module, then renders a read-only
|
||||
`HEAD:examples/flex-reference.ebox` snapshot without touching the working-tree
|
||||
fixture. The GUI driver performs five warmups followed by 30 accepted viewport
|
||||
updates; requested and published widths must match exactly, and both p95 and max
|
||||
must remain at or below 50ms. For uncontaminated repeated samples, run
|
||||
`make performance-prepare`, let compilation activity settle, then run
|
||||
`make performance-evaluator` without rebuilding.
|
||||
|
||||
200
README.zh-CN.md
200
README.zh-CN.md
@ -1,24 +1,214 @@
|
||||
# ebox-playground
|
||||
|
||||
`ebox-playground` 是 Ebox DSL 的独立通用文件运行器,只使用 Ebox 公共 API;具体布局都放在 `examples/` 下易读的 `.ebox` fixture 中。迁移后的低层 fixture 包含原有的 Basic、Comprehensive、Flex 和 Responsive 参考;`grid-reference.ebox` 则用同一套克制的陶土色、鼠尾草绿、蓝色、赭石色和青绿色视觉语言,完整展示 Grid 公共属性及其值形式。
|
||||
`ebox-playground` 是 Ebox DSL 的独立通用文件运行器,只使用 Ebox 公共 API;具体布局都放在 `examples/` 下易读的 `.ebox` fixture 中。`flex-reference.ebox`、`grid-reference.ebox` 和 `size-reference.ebox` 用克制的陶土色、鼠尾草绿、蓝色、紫色、赭石色和青绿色视觉语言,展示布局与尺寸规则。`interaction-reference.ebox` 展示 Ebox 原生交互及 retained 更新。
|
||||
|
||||
ECSS 0.1.0 与 TP 1.0.0 是互相独立的包,安装顺序任意;两者都安装后再安装 Ebox 2.0.0,最后安装本包。`ebox-playground` 不调用这些依赖的私有 API。
|
||||
ECSS 0.1.0 与 TP 1.0.1 是互相独立的包,安装顺序任意;两者都安装后再安装 Ebox 2.0.1,最后安装 Ebox Playground 0.1.1。`ebox-playground` 不调用这些依赖的私有 API。
|
||||
|
||||
独立画廊入口默认使用明确的 720 px 画布,因此 batch 渲染和单窗口演示里的嵌套示例有稳定空间。分屏 `.ebox` 预览则会把 `(viewport)` 绑定为右侧预览窗口的显示安全宽度,所以依赖 viewport 的分隔条和区块会适配右侧窗格。高层 Component/Runtime 展示仍由独立的 `etaf-playground` 提供。
|
||||
独立画廊入口默认使用明确的 720 px 画布,因此 batch 渲染和单窗口演示里的嵌套示例有稳定空间。分屏 `.ebox` 预览则会将 `(vw 100)` 解析为右侧预览窗口的显示安全宽度。章节分隔条使用所在 Column 的可用宽度,因此根容器缩窄时也会跟随。高层 Component/Runtime 展示仍由独立的 `etaf-playground` 提供。
|
||||
|
||||
独立和 batch fixture 渲染默认也使用紧凑的 720 px 视口;如果调用方动态绑定了 `ebox-viewport-width`,则保留调用方的值。因此依赖 viewport 的参考例子也会保持在示例画布内。通过 `C-c C-c` 渲染 `.ebox` 源文件时,命令会把源文件保留在左侧、在右侧打开预览,并将 `(viewport)` 绑定为 Ebox 统一计算的显示安全宽度;Ebox 唯一且串行的 viewport controller 会立即通过 retained 路径发布每次 window-size 变化,Playground 不再拥有第二套 resize hook 或 timer。
|
||||
独立和 batch fixture 渲染默认也使用紧凑的 720 px 视口;如果调用方动态绑定了 `ebox-viewport-width`,则保留调用方的值。因此依赖 viewport 的参考例子也会保持在示例画布内。通过 `C-c C-c` 渲染 `.ebox` 源文件时,命令会把源文件保留在左侧、在右侧打开预览,并将 `(vw 100)` 解析为 Ebox 统一计算的显示安全宽度;Ebox 唯一且串行的 viewport controller 会立即通过 retained 路径发布每次 window-size 变化,Playground 不再拥有第二套 resize hook 或 timer。
|
||||
|
||||
```elisp
|
||||
(require 'ebox-playground)
|
||||
(ebox-playground-open)
|
||||
```
|
||||
|
||||
这个包也拥有 `.ebox` 文件入口。访问 Ebox DSL 文件时会自动进入 `ebox-dsl-mode`,header line 会显示 `C-c C-c Render preview`;按 `C-c C-c` 会通过 Ebox 公开 API 构建单个 DSL form、渲染并自动并排显示源文件和预览 buffer。
|
||||
打开 `examples/size-reference.ebox`,按 `C-c C-c` 即可查看 **Ebox Size Reference**。
|
||||
示例按允许的方向展示六种长度单位、六个尺寸约束属性支持的关键词、`calc/min/max/clamp`,以及字体、
|
||||
间距、Flex、Grid 中的组合用法。调整预览宽度可以对照百分比和视口单位,并观察流式尺寸的变化。
|
||||
精度和 buffer 渲染器的限制会直接标明,不把当前实现描述成与浏览器完全一致。
|
||||
受限容器里的长文本用于区分内容宽度关键词,Row 对照用于区分 `auto` 和 `stretch`。
|
||||
高度内容关键词会明确标注当前渲染器中的等效关系;`min`、`max`、`clamp` 使用相同的低、中、高输入展示边界变化。
|
||||
实色矩形标明被测 Box 的完整范围(包含空白),浅色标尺表示父容器的可用宽度,白色卡片承载说明。
|
||||
|
||||
运行 `make size-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`,可检查核心尺寸契约、
|
||||
示例的实际属性覆盖、已知计算结果、背景所指示的尺寸差异、完整渲染及保留状态的视口更新。`make check` 也包含此示例的回归测试。
|
||||
|
||||
<a id="native-interaction-lab"></a>
|
||||
## 原生交互实验室
|
||||
|
||||
使用已有文件运行器打开交互参考:
|
||||
|
||||
```elisp
|
||||
(require 'ebox-playground)
|
||||
(ebox-playground-open-file
|
||||
(expand-file-name "examples/interaction-reference.ebox"
|
||||
ebox-playground-directory))
|
||||
```
|
||||
|
||||
也可以访问 `examples/interaction-reference.ebox` 后按 `C-c C-c`,并排显示源码和
|
||||
预览。**Ebox Native Interaction Lab** 先分别展示帮助文字、指针形状与悬停绘制,
|
||||
再通过实时计数器和事件日志组合 `:help-echo`、`:pointer`、`:hover-style` 与 `:keymap`。
|
||||
|
||||
- 悬停在带边框的卡片上(包括 padding),对比静态帮助、动态帮助、原生指针以及
|
||||
颜色和文本装饰变化。
|
||||
- 点击 **ADD / [+]** 增加,点击独立的 **[-]** Text 减少;把 point 移到对应操作上
|
||||
使用 `RET` / `SPC`,也可以使用其 `+` / `-` 快捷键。
|
||||
**SWITCH STEP 1 / 5** 将其 keymap 替换为使用对应步长的命令。
|
||||
- **SWITCH HOVER PALETTE** 替换悬停绘制;**REMOVE / RESTORE** 通过显式
|
||||
`nil` 清除四种能力,再恢复它们。Text 重置操作可以清零计数。
|
||||
- **UPDATE STATUS GROUP** 通过一次公共 `ebox-update-selector` 调用,共同更新
|
||||
三个 `.demo-status` Box。
|
||||
- 嵌套区域展示父节点覆盖、拥有独立命令的 Text 子节点,以及通过显式 `nil` 阻止
|
||||
外层能力覆盖的子节点。事件日志记录实际运行的命令。
|
||||
|
||||
Box 的帮助、指针和 keymap 范围包含内容、padding 和 border,不包含该 Box 自身的
|
||||
margin 及结构性换行符。同一渲染行内,一个 hover owner 的文字与 padding 共享原生
|
||||
`mouse-face`;未指定属性取自 owner 的基础样式,即使子文本的正常样式不同。
|
||||
物理左右边框不参与悬停,水平边框线条保留自己的绘制。原生指针外观和帮助显示由
|
||||
Emacs 设置及窗口系统决定,
|
||||
见[公共能力契约](../ebox/docs/user/ebox-api-reference.zh.md#原生节点能力)。
|
||||
|
||||
`interaction-reference.el` 负责业务函数与状态,`interaction-reference.ebox` 负责
|
||||
布局;运行器在每次预览前重新加载对应 `.el`。Ebox 的 `ebox-help-create` 提供原生
|
||||
帮助适配,`ebox-keymap-create` 绑定
|
||||
零参数回调,使用 `ebox-region-update` 更新命名 region;鼠标回调自动在事件窗口的
|
||||
buffer 中运行。Ebox 不增加应用状态或焦点导航系统,使用键盘绑定时通过普通 point
|
||||
移动定位。Keymap 会创建快照,发布新绑定需要替换 `:keymap`。每次求值都会创建
|
||||
独立的新示例。
|
||||
|
||||
在本目录运行交互检查:
|
||||
|
||||
```sh
|
||||
make interaction-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||
```
|
||||
|
||||
该入口验证核心交互契约、示例渲染后的能力、命令效果、替换与清除/恢复。
|
||||
`make check` 也包含 Playground 交互回归测试。
|
||||
|
||||
检查重复按键行为和命令耗时时,使用共享交互 evaluator:
|
||||
|
||||
```sh
|
||||
emacs -Q --batch -L . -L ../ebox -L ../ecss -L ../tp \
|
||||
-l scripts/ebox-playground-flex-resize-evaluator.el \
|
||||
--eval '(pp (ebox-playground-interaction-evaluator-run "examples/interaction-reference.ebox" "ADD " "RET" 30 720 200))'
|
||||
```
|
||||
|
||||
函数签名为 `(FILE LABEL &optional KEY STEPS WIDTH HEIGHT)`。它创建全新的临时
|
||||
预览,定位一次可见标签,然后在该 point 位置反复查找原生按键绑定,不在更新后重新
|
||||
定位;失去节点绑定时检查失败。`KEY` 接受按键描述字符串或事件向量,默认参数为
|
||||
`RET`、30 次命令、720 px 宽、200 行高;次数与尺寸必须为正整数。
|
||||
|
||||
返回的 plist 包含命令耗时的 mean/p95/max 毫秒数、GC 次数与耗时、cons 分配总数
|
||||
(`:cons-cells`,用于观察分配压力)、发布次数、最终
|
||||
纯文本,以及与独立渲染已提交快照的结果是否一致。计算计数器 `:layout-calls`、
|
||||
`:surface-plans`、`:fragment-scans` 和 `:node-registrations` 分别统计布局渲染、
|
||||
surface 规划、fragment 提取(包含局部切片)和节点树注册的调用次数。
|
||||
`:fragment-characters` 累计传给 fragment 提取的字符数;结合调用次数与字符总量,
|
||||
区分局部处理和全 surface 扫描。调用次数不是受影响节点数。快照对比不一致则检查失败。
|
||||
|
||||
计时包括同步命令执行、发布和 GC。耗时、GC、发布次数及计算计数器都只覆盖命令
|
||||
循环,不包括初次挂载及最后的快照对比渲染。该入口也不测量 GUI redisplay、
|
||||
操作系统按键重复延迟或空闲预热。采样循环在命令之间检查 30 秒期限;成功、报错、
|
||||
超时或中断时都会清理临时预览和自身计时器,并保留现有预览与窗口。后续交互回归
|
||||
复用此入口,不另建临时的重复按键计时脚本。
|
||||
|
||||
诊断已有可见预览中的原生点击分派时,加载同一 evaluator 并调用
|
||||
`(ebox-playground-interaction-evaluator-click BUFFER LABEL &optional OFFSET)`。
|
||||
`BUFFER` 是 live buffer 或其名称;第一个匹配标签必须位于显示窗口的可见区域内。
|
||||
`OFFSET` 是标签内从零开始的整数字符偏移,默认为零。例如,在预览 buffer 中求值:
|
||||
|
||||
```elisp
|
||||
(load (expand-file-name "scripts/ebox-playground-flex-resize-evaluator.el"
|
||||
ebox-playground-directory) nil t t)
|
||||
(ebox-playground-interaction-evaluator-click (current-buffer) "ADD " 0)
|
||||
(ebox-playground-interaction-evaluator-click (current-buffer) "[-]" 1)
|
||||
```
|
||||
|
||||
它构造 Emacs 鼠标事件,查找原生绑定并通过 `call-interactively` 调用,不注入操作
|
||||
系统鼠标点击。辅助函数自身不切换窗口或移动 point,返回被点击的位置;业务结果需
|
||||
另行通过计数器或事件日志检查。
|
||||
|
||||
## 编写和渲染 `.ebox` 文件
|
||||
|
||||
这个包也拥有 `.ebox` 文件入口。访问 Ebox DSL 文件时会自动进入 `ebox-dsl-mode`,header line 会显示 `C-c C-c Render preview`;按 `C-c C-c` 会求值文件中的唯一一个 Elisp 表达式,将得到的布局数据传给 `ebox-build`,渲染并自动并排显示源文件和预览 buffer。
|
||||
|
||||
```elisp
|
||||
(add-to-list 'auto-mode-alist '("\\.ebox\\'" . ebox-dsl-mode))
|
||||
```
|
||||
|
||||
`.ebox` 中写的表达式与 Elisp 中传给 `(ebox-build ...)` 的参数完全一致。
|
||||
静态布局对整个列表加 quote,内部的 list 和 symbol 属性不再单独加 quote:
|
||||
|
||||
```elisp
|
||||
'(column :padding ((lh 1) (ch 2))
|
||||
:cross-align center
|
||||
(box :width (px 240) "Hello"))
|
||||
```
|
||||
|
||||
必要的业务函数与变量放在 `.ebox` 旁边可选的同名 `.el` 文件中。每次预览求值前,
|
||||
运行器先加载存在的对应 `.el` 源码,再求值 `.ebox` 表达式。`C-c C-c`、
|
||||
`ebox-playground-open-file` 和文件渲染入口都遵循这个顺序;保存 `.el` 修改后再次
|
||||
预览即可生效,不使用 `require` 缓存,也不选择旧 bytecode。允许没有配套文件;
|
||||
配套文件加载出错则停止求值。普通 Elisp 变量定义决定重新加载时保留还是重置状态。
|
||||
|
||||
例如,把数据与操作放在 `notes.el`:
|
||||
|
||||
```elisp
|
||||
;;; notes.el --- Notes example behavior -*- lexical-binding: t; -*-
|
||||
(defvar notes-body "A long article.")
|
||||
|
||||
(defun notes-help ()
|
||||
"Return business help for the article."
|
||||
(format "Article: %s" notes-body))
|
||||
|
||||
(defun notes-activate ()
|
||||
"Mark the article in the current Ebox buffer."
|
||||
(ebox-region-update "article" :color "#166534"))
|
||||
```
|
||||
|
||||
`notes.ebox` 专注表达布局:
|
||||
|
||||
```elisp
|
||||
`(column :padding ((lh 1) (ch 2))
|
||||
(box :id "article" :width (px 480)
|
||||
:help-echo ,(ebox-help-create #'notes-help)
|
||||
:pointer hand
|
||||
:keymap ,(ebox-keymap-create :activate #'notes-activate)
|
||||
,notes-body))
|
||||
```
|
||||
|
||||
`ebox-help-create` 把返回字符串或 `nil` 的零参数业务函数适配为原生帮助,并提供悬停
|
||||
buffer 上下文。`ebox-keymap-create` 的 `:activate` 处理 `RET`、`[return]`、`SPC`
|
||||
和 `[mouse-1]`。
|
||||
可选的 `:bindings` alist 将按键描述字符串或事件向量与零参数回调配对;也可以直接
|
||||
传入原生 keymap。详见[公共辅助函数契约](../ebox/docs/user/ebox-api-reference.zh.md#原生节点能力)。
|
||||
|
||||
运行器每次渲染都以词法绑定求值一个 `.ebox` 表达式。反引号保留布局数据,`,` 插入
|
||||
值,`,@` 展开子节点列表。结果中的属性和子节点都是数据,运行器不会再单独求值。
|
||||
业务定义留在配套文件中;原生帮助和命令适配使用 Ebox 公共接口,不要仅为了隐藏
|
||||
重复 DSL 属性列表而添加辅助函数。
|
||||
|
||||
默认采用简洁写法:省略冗余默认值,把可继承的共同文字样式放到已有父节点,子节点只写差异。
|
||||
示例正在讲解的属性仍显式保留。[编写原则](../ebox/docs/user/ebox-user-guide.zh.md#优先利用默认值和继承)
|
||||
说明了哪些属性可以继承。空 Box 的自动内容高度为零;一行分隔框显式设置 `:height (lh 1)`。
|
||||
|
||||
简化示例时,使用已有 evaluator 对比保存的基准目录和当前 `examples` 目录:
|
||||
|
||||
```sh
|
||||
emacs -Q --batch -L . -L ../ebox -L ../ecss -L ../tp \
|
||||
-l scripts/ebox-playground-flex-resize-evaluator.el \
|
||||
--eval '(ebox-playground-compare-example-directories "/path/to/baseline" "examples")'
|
||||
```
|
||||
|
||||
可选的第三、第四参数指定视口宽度和文件名;默认检查两个示例在 720、1000 px 下的首屏与全文。
|
||||
比较内容包括文字、有效 face、display、交互属性和逐行宽度,发现差异时 batch 命令失败。
|
||||
该入口不创建文件或 GUI 窗口,后续同类任务直接复用它,不再编写一次性比较脚本。
|
||||
|
||||
尺寸数据统一使用 `(单位 数值)`,支持 `px`、`%`、`vw`、`vh`、`ch`、`lh`,
|
||||
以及 `calc`、`min`、`max`、`clamp`。[统一单位约束表](../ebox/docs/user/ebox-user-guide.zh.md#size-unit-constraints)
|
||||
规定每个属性允许的单位;不能跨轴混用,嵌套函数的所有分支也遵循同一规则。
|
||||
无效组合在 Ebox 构造树时就报错。边框厚度采用表中独立的绘制单位规则。
|
||||
例如比视口少一个行高,写
|
||||
`:height (calc (- (vh 100) (lh 1)))`;Ebox 会在每次布局时解析这个表达式。
|
||||
`fit-content` 只作为裸关键词使用,不带参数。Ebox buffer 后端的纵向内容仍按
|
||||
完整行量化,具体规则见 [Ebox 尺寸契约](../ebox/docs/user/ebox-user-guide.zh.md#4-使用几何与绘制-property)。
|
||||
|
||||
迁移旧的裸结构 `.ebox` 时,静态布局在最外层加 quote,并去掉内部 list 和 symbol
|
||||
属性的 quote,例如 `:padding '(1 2)` 改成 `:padding ((lh 1) (ch 2))`。动态值使用
|
||||
反引号和显式逗号,例如计算像素宽度时写 `:width (px ,(+ 200 40))`。
|
||||
裸长度、单元素像素列表和旧的视口关键词也需要迁移;运行器不自动识别旧格式。
|
||||
|
||||
参考 fixture 直接位于 `examples/` 下。例如:
|
||||
|
||||
```elisp
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
|
||||
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||
;; Author: Ebox contributors
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "29.1") (ebox "2.0.0"))
|
||||
;; Version: 0.1.1
|
||||
;; Package-Requires: ((emacs "29.1") (ebox "2.0.1"))
|
||||
;; URL: https://github.com/ginqi7/ebox-playground
|
||||
|
||||
;;; Commentary:
|
||||
@ -35,10 +35,11 @@
|
||||
"Header line shown by `ebox-dsl-mode'.")
|
||||
|
||||
(defun ebox-playground--source-form ()
|
||||
"Read the single Ebox DSL form from the current buffer."
|
||||
"Read and evaluate the single Elisp expression in the current buffer."
|
||||
(ebox-playground--evaluate-form
|
||||
(ebox-playground--read-form
|
||||
(buffer-substring-no-properties (point-min) (point-max)))))
|
||||
(buffer-substring-no-properties (point-min) (point-max)))
|
||||
buffer-file-name))
|
||||
|
||||
;;;###autoload
|
||||
(define-derived-mode ebox-dsl-mode emacs-lisp-mode "EboxDSL"
|
||||
@ -90,7 +91,7 @@
|
||||
"Directory containing the Ebox Playground package.")
|
||||
|
||||
(defcustom ebox-playground-default-file
|
||||
(expand-file-name "examples/public-layout-gallery.ebox"
|
||||
(expand-file-name "examples/grid-reference.ebox"
|
||||
ebox-playground-directory)
|
||||
"Ebox DSL file rendered by `ebox-playground-open'."
|
||||
:type 'file
|
||||
@ -114,12 +115,9 @@
|
||||
((ebox-playground--window-viewport-width window))
|
||||
(t ebox-playground-default-viewport-width)))
|
||||
|
||||
(defun ebox-playground--apply-preview-display-settings (&optional canvas-background)
|
||||
"Apply canvas redisplay settings to the current preview buffer.
|
||||
CANVAS-BACKGROUND fills display space outside generated root lines."
|
||||
(defun ebox-playground--apply-preview-display-settings ()
|
||||
"Apply text redisplay settings without overriding buffer chrome or faces."
|
||||
(setq-local truncate-lines t)
|
||||
(setq-local header-line-format nil)
|
||||
(setq-local mode-line-format nil)
|
||||
(setq-local auto-hscroll-mode t)
|
||||
(setq-local fringe-indicator-alist
|
||||
(cl-remove-if
|
||||
@ -128,15 +126,12 @@ CANVAS-BACKGROUND fills display space outside generated root lines."
|
||||
(copy-tree fringe-indicator-alist)))
|
||||
(setq-local bidi-display-reordering nil)
|
||||
(setq-local bidi-paragraph-direction 'left-to-right)
|
||||
(setq-local bidi-inhibit-bpa t)
|
||||
(setq-local face-remapping-alist
|
||||
(when (stringp canvas-background)
|
||||
(list (list 'default (list :background canvas-background))))))
|
||||
(setq-local bidi-inhibit-bpa t))
|
||||
|
||||
(defconst ebox-playground--preview-display-variables
|
||||
'(truncate-lines header-line-format mode-line-format auto-hscroll-mode
|
||||
'(truncate-lines auto-hscroll-mode
|
||||
fringe-indicator-alist bidi-display-reordering bidi-paragraph-direction
|
||||
bidi-inhibit-bpa face-remapping-alist)
|
||||
bidi-inhibit-bpa)
|
||||
"Buffer-local display variables owned by a playground preview.")
|
||||
|
||||
(defun ebox-playground--preview-display-snapshot ()
|
||||
@ -158,82 +153,59 @@ CANVAS-BACKGROUND fills display space outside generated root lines."
|
||||
(set (make-local-variable variable) value)
|
||||
(kill-local-variable variable)))))
|
||||
|
||||
(defun ebox-playground--render-to-preview-buffer (buffer node)
|
||||
"Render NODE into BUFFER with transactional preview display settings."
|
||||
(defun ebox-playground--render-to-preview-buffer (buffer input)
|
||||
"Render canonical INPUT into BUFFER with transactional display settings."
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(with-current-buffer (get-buffer-create buffer)
|
||||
(let ((snapshot (ebox-playground--preview-display-snapshot))
|
||||
(published-p nil))
|
||||
(unwind-protect
|
||||
(prog1
|
||||
(progn
|
||||
(ebox-playground--apply-preview-display-settings
|
||||
(and (consp node)
|
||||
(ebox-style-node-specified-value
|
||||
node :background-color)))
|
||||
(ebox-render-to-buffer buffer node))
|
||||
(ebox-playground--apply-preview-display-settings)
|
||||
(ebox-render-to-buffer buffer input))
|
||||
(setq published-p t))
|
||||
(unless published-p
|
||||
(ebox-playground--restore-preview-display-snapshot snapshot))))))
|
||||
|
||||
(defun ebox-playground--render-node-to-buffer (buffer node viewport-width)
|
||||
"Render NODE into BUFFER using VIEWPORT-WIDTH.
|
||||
(defun ebox-playground--render-input-to-buffer (buffer input viewport-width)
|
||||
"Render canonical INPUT into BUFFER using VIEWPORT-WIDTH.
|
||||
Visible mounted buffers follow their window through Ebox's viewport controller."
|
||||
(let ((ebox-viewport-width
|
||||
(or viewport-width
|
||||
(ebox-playground--effective-viewport-width))))
|
||||
(ebox-playground--render-to-preview-buffer buffer node)))
|
||||
(ebox-playground--render-to-preview-buffer buffer input)))
|
||||
|
||||
(defun ebox-playground--static-value-p (value)
|
||||
"Return non-nil when VALUE is a literal or explicitly quoted form."
|
||||
(or (null value) (numberp value) (stringp value) (characterp value)
|
||||
(keywordp value) (eq value t)
|
||||
(and (consp value) (memq (car value) '(quote function)))))
|
||||
|
||||
(defun ebox-playground--evaluate-value (value)
|
||||
"Evaluate executable DSL VALUE while preserving literal data values."
|
||||
(if (ebox-playground--static-value-p value)
|
||||
(if (and (consp value) (memq (car value) '(quote function)))
|
||||
(eval value nil)
|
||||
value)
|
||||
(eval value nil)))
|
||||
|
||||
(defun ebox-playground--evaluate-items (items)
|
||||
"Evaluate property values and recurse into child forms in ITEMS."
|
||||
(let (result)
|
||||
(while items
|
||||
(let ((item (pop items)))
|
||||
(if (keywordp item)
|
||||
(progn
|
||||
(unless items
|
||||
(error "Ebox playground: missing value for %S" item))
|
||||
(push item result)
|
||||
(push (ebox-playground--evaluate-value (pop items)) result))
|
||||
(push (ebox-playground--evaluate-form item) result))))
|
||||
(nreverse result)))
|
||||
|
||||
(defun ebox-playground--evaluate-form (form)
|
||||
"Evaluate property expressions in one structural DSL FORM."
|
||||
(if (and (consp form) (symbolp (car form)))
|
||||
(cons (car form) (ebox-playground--evaluate-items (cdr form)))
|
||||
form))
|
||||
(defun ebox-playground--evaluate-form (form &optional source-file)
|
||||
"Evaluate one Elisp FORM lexically to produce Ebox DSL data.
|
||||
When SOURCE-FILE names a .ebox file, load its optional same-basename .el
|
||||
companion first. Reload that exact source file on every evaluation."
|
||||
(when (and source-file
|
||||
(equal (file-name-extension source-file) "ebox"))
|
||||
(let ((companion (concat (file-name-sans-extension
|
||||
(expand-file-name source-file)) ".el")))
|
||||
(when (file-exists-p companion)
|
||||
(load companion nil t t))))
|
||||
(eval form t))
|
||||
|
||||
(defun ebox-playground--read-form (source)
|
||||
"Read one Ebox DSL form from SOURCE."
|
||||
"Read exactly one Elisp expression from SOURCE."
|
||||
(when (string-empty-p (string-trim source))
|
||||
(user-error "The Ebox DSL source is empty"))
|
||||
(with-temp-buffer
|
||||
(set-syntax-table emacs-lisp-mode-syntax-table)
|
||||
(insert source)
|
||||
(goto-char (point-min))
|
||||
(let ((read-eval nil)
|
||||
(form (read (current-buffer))))
|
||||
(condition-case nil
|
||||
(progn
|
||||
(read (current-buffer))
|
||||
(user-error "The Ebox DSL source must contain one form"))
|
||||
(end-of-file form)))))
|
||||
(let* ((read-eval nil)
|
||||
(form (read (current-buffer))))
|
||||
(forward-comment (point-max))
|
||||
(unless (eobp)
|
||||
(user-error "The Ebox source must contain one Elisp expression"))
|
||||
form)))
|
||||
|
||||
(defun ebox-playground--read-file (file)
|
||||
"Read one Ebox DSL form from FILE."
|
||||
"Read one Elisp expression from FILE."
|
||||
(unless (file-readable-p file)
|
||||
(user-error "Ebox DSL file is not readable: %s" file))
|
||||
(with-temp-buffer
|
||||
@ -243,20 +215,21 @@ Visible mounted buffers follow their window through Ebox's viewport controller."
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-playground-view (&optional file viewport-width)
|
||||
"Build the Ebox DSL form in FILE at VIEWPORT-WIDTH without buffer side effects."
|
||||
"Build the Ebox DSL form in FILE at VIEWPORT-WIDTH without publishing it.
|
||||
Reload FILE's optional same-basename .el companion before evaluating its DSL."
|
||||
(let ((ebox-viewport-width
|
||||
(or viewport-width (ebox-playground--effective-viewport-width))))
|
||||
(or viewport-width (ebox-playground--effective-viewport-width)))
|
||||
(file (expand-file-name (or file ebox-playground-default-file))))
|
||||
(ebox-build
|
||||
(ebox-playground--evaluate-form
|
||||
(ebox-playground--read-file
|
||||
(expand-file-name (or file ebox-playground-default-file)))))))
|
||||
(ebox-playground--read-file file) file))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-playground-open (&optional buffer-name)
|
||||
"Render the default Ebox DSL example into BUFFER-NAME and return its buffer."
|
||||
(interactive)
|
||||
(let ((viewport-width (ebox-playground--effective-viewport-width)))
|
||||
(ebox-playground--render-node-to-buffer
|
||||
(ebox-playground--render-input-to-buffer
|
||||
(or buffer-name ebox-playground-buffer-name)
|
||||
(ebox-playground-view nil viewport-width)
|
||||
viewport-width)))
|
||||
@ -266,7 +239,7 @@ Visible mounted buffers follow their window through Ebox's viewport controller."
|
||||
"Render Ebox DSL FILE into BUFFER-NAME and return its buffer."
|
||||
(interactive "fEbox DSL file: ")
|
||||
(let ((viewport-width (ebox-playground--effective-viewport-width)))
|
||||
(ebox-playground--render-node-to-buffer
|
||||
(ebox-playground--render-input-to-buffer
|
||||
(or buffer-name
|
||||
(format "*Ebox Preview: %s*" (file-name-nondirectory file)))
|
||||
(ebox-playground-view file viewport-width)
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
# Ebox Playground examples
|
||||
|
||||
The `.ebox` files in this directory are the concrete layouts consumed by the generic `ebox-playground` runner. They are intentionally ordinary, reader-valid Ebox DSL forms so authors can copy, edit, and open them directly.
|
||||
|
||||
The file itself is the data form: structural forms stay unquoted, while literal list and symbol property values use quote just as they do in `etaf-view` (for example, `:gap '(1 (16))` and `:justify-content 'center`). An unquoted property value is an executable Elisp expression and is evaluated by the runner. The reader itself keeps read-time evaluation disabled.
|
||||
|
||||
`public-layout-gallery.ebox` is the default fixture used by `M-x ebox-playground-open`. The migrated low-level references live directly beside it: `basic.ebox`, `comprehensive.ebox`, `flex-reference.ebox`, `responsive-reference.ebox`, and `grid-reference.ebox`. The Flex reference owns its document viewport with a root `:overflow 'scroll` box: keyboard line scrolling follows the cursor's innermost scroll owner, mouse wheel scrolling follows the pointer, and residual movement bubbles through nested owners before native buffer scrolling. It keeps its original complete-content semantics during splitter resize. The Grid reference is the complete public property/value gallery: track templates (`auto`, `NFR`, `(fr FACTOR)`, `minmax`, and `repeat`), implicit tracks, row/column auto-flow, gap shorthand/longhands/aliases, placement and spans, and every supported item/content alignment value. It also documents that Ebox supports `row` and `column` auto-flow rather than browser `dense` variants. To render another fixture, visit it and press `C-c C-c`, or call `ebox-playground-open-file`.
|
||||
@ -1,7 +0,0 @@
|
||||
# Ebox Playground 示例
|
||||
|
||||
此目录下的 `.ebox` 文件是通用 `ebox-playground` 运行器读取的具体布局。它们保持为普通、可读、可被 reader 读取的 Ebox DSL form,便于直接复制、编辑和打开。
|
||||
|
||||
文件本身就是 data form:结构表单保持不加 quote;list 和 symbol 类型的字面量属性值要和 `etaf-view` 一样加 quote,例如 `:gap '(1 (16))`、`:justify-content 'center`。未加 quote 的属性值会被运行器当作可执行的 Elisp 表达式求值;读取器本身仍然关闭 reader 求值。
|
||||
|
||||
`public-layout-gallery.ebox` 是 `M-x ebox-playground-open` 使用的默认 fixture。迁移来的低层参考和它并列放在 `examples/` 下:`basic.ebox`、`comprehensive.ebox`、`flex-reference.ebox`、`responsive-reference.ebox` 和 `grid-reference.ebox`。Flex 参考由根部 `:overflow 'scroll` 盒子拥有文档 viewport:键盘逐行作用于光标所在的最内层滚动 owner,鼠标滚轮按指针位置路由,剩余滚动量沿嵌套 owner 向外冒泡,最后才交给普通 buffer;splitter resize 时仍保持原有的完整内容语义。Grid 参考是完整的公共属性/值画廊:轨道模板(`auto`、`NFR`、`(fr FACTOR)`、`minmax`、`repeat`)、隐式轨道、row/column auto-flow、gap 简写/长写/别名、placement 与 span,以及所有受支持的 item/content 对齐值;同时注明 Ebox 支持 `row` 和 `column` auto-flow,不支持浏览器的 `dense` 变体。要渲染其他 fixture,可以访问该文件后按 `C-c C-c`,或调用 `ebox-playground-open-file`。
|
||||
@ -1,3 +0,0 @@
|
||||
(box :box-sizing 'border-box :padding '(1 (12)) :border t
|
||||
:border-color "#2563EB" :bgcolor "#EFF6FF" :color "#1E3A8A"
|
||||
"Playground file")
|
||||
@ -1,29 +0,0 @@
|
||||
(column
|
||||
(box :padding '(1 (16)) :border "#334155" :bgcolor "#F8FAFC" :color
|
||||
"#0F172A" :text-align 'center
|
||||
"Ebox comprehensive DSL showcase\nThe outer sections omit :width so Playground viewport sizing drives the layout.")
|
||||
(box :width '(viewport) :height 1 :bgcolor "#F8FAFC")
|
||||
(box :padding '(0 (14)) :border "#64748B" :bgcolor "#F8FAFC" :color
|
||||
"#111827" :wrap-mode 'word
|
||||
"Width rule in this file\nContainer sections use auto width. Fixed widths remain only where the example demonstrates exact item sizing, basis, min-width, or max-width.")
|
||||
(box :width '(viewport) :height 1 :bgcolor "#F8FAFC")
|
||||
(flex :margin '(1 0 0 1) :padding '(1 (8)) :border "#475569" :bgcolor
|
||||
"#F8FAFC" :flex-flow '(row wrap) :justify-content
|
||||
'space-between :align-items 'center :align-content 'flex-start
|
||||
:row-gap 1 :column-gap '(12)
|
||||
(box :order -1 :flex-basis '(270) :align-self 'flex-start
|
||||
:width '(270) :padding '(0 (8)) :border "#BE185D"
|
||||
:bgcolor "#FCE7F3" :color "#831843" :wrap-mode 'word
|
||||
"Box order -1\n:flex-basis 270\nalign-self start")
|
||||
(box :flex '(1 1 (220)) :width '(220) :height 4 :padding
|
||||
'(0 (8)) :border "#1D4ED8" :bgcolor "#DBEAFE" :color
|
||||
"#111827" :wrap-mode 'word
|
||||
"Box :flex (1 1 220)\ngrow shrink basis")
|
||||
(box :flex-grow 2 :flex-shrink 1 :flex-basis '(240) :align-self
|
||||
'center :width '(240) :height 4 :padding '(0 (8)) :border
|
||||
"#B45309" :bgcolor "#FEF3C7" :color "#78350F" :wrap-mode
|
||||
'word "Box grow 2\nshrink 1\nbasis 240")
|
||||
(box :flex-shrink 3 :flex-basis '(190) :align-self 'flex-end
|
||||
:width '(190) :height 4 :padding '(0 (8)) :border
|
||||
"#15803D" :bgcolor "#DCFCE7" :color "#14532D" :wrap-mode
|
||||
'word "Box shrink 3\nbasis 190\nalign-self flex-end")))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
136
examples/interaction-reference.ebox
Normal file
136
examples/interaction-reference.ebox
Normal file
@ -0,0 +1,136 @@
|
||||
;; Layout and property declarations. The matching .el file supplies state
|
||||
;; and business actions; the runner loads it before evaluating this expression.
|
||||
(let ((demo (interaction-demo-create)))
|
||||
`(column :width (vw 100) :height (vh 95)
|
||||
:padding ((lh 1) (ch 2))
|
||||
:background-color "#F4F3ED" :color "#273B42"
|
||||
(box :background-color "#153F4A" :color "#FFFFFF"
|
||||
:padding ((lh 1) (ch 2))
|
||||
(text :font-weight bold "EBOX / NATIVE INTERACTION LAB")
|
||||
"\nFour node properties. Real commands. Incremental updates.")
|
||||
(box :padding-block (lh 1)
|
||||
"Wheel / C-v scroll. Hover to inspect. Click a control, or move point into it and press RET / SPC.\n"
|
||||
"Solid borders mark Box surfaces, including padding. Gaps belong to no control.\n"
|
||||
"Each section below isolates an effect; the live console combines all four.")
|
||||
|
||||
(box :font-weight bold "01 HELP + POINTER / hints without actions")
|
||||
(flex :flex-wrap wrap :gap ((lh 1) (ch 2)) :padding-block (lh 1)
|
||||
(box :width (ch 22) :padding ((lh 1) (ch 1))
|
||||
:border "#90A9AB" :background-color "#FFFFFF"
|
||||
:help-echo "Static help: this entire bordered Box supplies the same hint."
|
||||
:pointer arrow
|
||||
"ARROW\nStatic Box help")
|
||||
(box :width (ch 22) :padding ((lh 1) (ch 1))
|
||||
:border "#90A9AB" :background-color "#FFFFFF"
|
||||
"TEXT POINTER\n"
|
||||
(text :pointer text
|
||||
:help-echo "This help belongs only to the underlined text, not its surrounding card."
|
||||
:text-decoration-line underline
|
||||
"Text-only hint"))
|
||||
(box :width (ch 22) :padding ((lh 1) (ch 1))
|
||||
:border "#90A9AB" :background-color "#FFFFFF"
|
||||
:pointer hdrag
|
||||
:help-echo "Pointer shape only: this is not a draggable splitter."
|
||||
"HORIZONTAL DRAG\nShape demonstration"))
|
||||
|
||||
(box :font-weight bold "02 HOVER STYLE / appearance, no command")
|
||||
(flex :flex-wrap wrap :gap ((lh 1) (ch 2)) :padding-block (lh 1) :pointer arrow
|
||||
(box :id "hover-cool" :width (ch 22) :padding ((lh 1) (ch 1))
|
||||
:border "#5893A3" :background-color "#E2EFF2"
|
||||
:help-echo "Cool hover changes foreground/background. Font size and Box size stay fixed."
|
||||
:hover-style (:color "#FFFFFF" :background-color "#286477")
|
||||
"COOL FILL\nMove over the padding too")
|
||||
(box :id "hover-warm" :width (ch 22) :padding ((lh 1) (ch 1))
|
||||
:border "#B8874D" :background-color "#F8EDDC"
|
||||
:help-echo "Warm hover adds an underline as well as color."
|
||||
:hover-style (:color "#683D16" :background-color "#F6D6AB"
|
||||
:text-decoration-line underline)
|
||||
"WARM + UNDERLINE\nNo layout movement")
|
||||
(box :width (ch 22) :padding ((lh 1) (ch 1))
|
||||
:border "#A7ABA5" :background-color "#E9EAE5"
|
||||
:help-echo "Control sample: no hover-style declaration."
|
||||
"NO HOVER STYLE\nCompare the three cards"))
|
||||
|
||||
(box :font-weight bold "03 LIVE CONSOLE / native keymaps + region updates")
|
||||
(box :padding ((lh 1) (ch 1)) :border "#5893A3"
|
||||
:background-color "#FFFFFF"
|
||||
(box :id "count" :color "#205A6C" :font-weight bold "COUNT 00")
|
||||
(box :id "binding-state" "step 1 · four properties attached · cool hover")
|
||||
(box :height (lh 1))
|
||||
(flex :flex-wrap wrap :gap ((lh 1) (ch 2))
|
||||
(box :id "live-action" :width (ch 23) :padding ((lh 1) (ch 1))
|
||||
:border "#286477" :background-color "#E2EFF2"
|
||||
:help-echo ,(ebox-help-create
|
||||
(apply-partially #'interaction-demo-description demo))
|
||||
:pointer hand
|
||||
:hover-style ,(interaction-demo-hover demo) :keymap ,(interaction-demo-keymap demo)
|
||||
(text :id "action-label" "ADD 1 [+] ")
|
||||
(text :id "subtract-text"
|
||||
:keymap ,(interaction-demo-subtract-keymap demo)
|
||||
:help-echo ,(ebox-help-create
|
||||
(apply-partially #'interaction-demo-description demo t))
|
||||
"[-]"))
|
||||
(box :id "step-control" :width (ch 23) :padding ((lh 1) (ch 1))
|
||||
:border "#90A9AB" :background-color "#F0F4F2"
|
||||
:pointer hand :help-echo "Replace the ADD control's keymap: +1 ↔ +5."
|
||||
:hover-style (:background-color "#CCE0D6")
|
||||
:keymap ,(interaction-demo-action demo 'step)
|
||||
"SWITCH STEP 1 / 5")
|
||||
(box :id "binding-control" :width (ch 23) :padding ((lh 1) (ch 1))
|
||||
:border "#90A9AB" :background-color "#F0F4F2"
|
||||
:pointer hand :help-echo "Remove or restore all four properties on the ADD control."
|
||||
:hover-style (:background-color "#CCE0D6")
|
||||
:keymap ,(interaction-demo-action demo 'binding)
|
||||
"REMOVE / RESTORE")
|
||||
(box :id "palette-control" :width (ch 23) :padding ((lh 1) (ch 1))
|
||||
:border "#B8874D" :background-color "#F8EDDC"
|
||||
:pointer hand :help-echo "Replace the ADD control's hover-style while preserving its geometry."
|
||||
:hover-style (:background-color "#F6D6AB")
|
||||
:keymap ,(interaction-demo-action demo 'palette)
|
||||
"SWITCH HOVER PALETTE")
|
||||
(box :id "group-control" :width (ch 23) :padding ((lh 1) (ch 1))
|
||||
:border "#789783" :background-color "#E9F0E9"
|
||||
:pointer hand :help-echo "Update all three .demo-status regions with one selector call."
|
||||
:hover-style (:background-color "#C8DDC9")
|
||||
:keymap ,(interaction-demo-action demo 'group)
|
||||
"UPDATE STATUS GROUP"))
|
||||
(row :item-gap (ch 2) :padding-block (lh 1)
|
||||
"Class targets:"
|
||||
(box :id "status-a" :class demo-status :color "#85652F" "WAIT A")
|
||||
(box :id "status-b" :class demo-status :color "#85652F" "WAIT B")
|
||||
(box :id "status-c" :class demo-status :color "#85652F" "WAIT C"))
|
||||
(box :padding-block (lh 1)
|
||||
"Text can be interactive too: "
|
||||
(text :id "reset-text" :pointer hand :keymap ,(interaction-demo-action demo 'reset)
|
||||
:help-echo "Reset count through a Text node's keymap."
|
||||
:color "#286477" :text-decoration-line underline
|
||||
:hover-style (:color "#9A531C")
|
||||
"[ RESET COUNT ]")))
|
||||
|
||||
(box :height (lh 1))
|
||||
(box :font-weight bold "04 NESTED OWNERSHIP / parent, child, explicit nil")
|
||||
(box :id "parent-surface" :padding ((lh 1) (ch 2))
|
||||
:border "#789783" :background-color "#E9F0E9"
|
||||
:pointer hand :help-echo "Parent surface: clicking its padding records PARENT."
|
||||
:hover-style (:background-color "#C8DDC9")
|
||||
:keymap ,(interaction-demo-action demo 'parent)
|
||||
"PARENT SURFACE — click here or in its padding\n"
|
||||
(text :id "child-text" :pointer hand
|
||||
:help-echo "Child owns its binding: this records CHILD, not PARENT."
|
||||
:hover-style (:color "#FFFFFF" :background-color "#466B55")
|
||||
:keymap ,(interaction-demo-action demo 'child)
|
||||
"[ CHILD ACTION ]")
|
||||
" "
|
||||
(text :id "quiet-text" :help-echo nil :pointer nil
|
||||
:hover-style nil :keymap nil
|
||||
"[ EXPLICIT NIL: no inherited interaction ]"))
|
||||
|
||||
(box :height (lh 1))
|
||||
(box :font-weight bold "EVENT LOG / newest first")
|
||||
(box :id "event-log" :height (lh 4) :padding-inline (ch 1)
|
||||
:background-color "#153F4A" :color "#E2EFF2"
|
||||
"No actions yet.\n—\n—\n—")
|
||||
(box :padding-block (lh 1) :color "#586B6A"
|
||||
"Commands update named regions; they do not rewrite buffer text.\n"
|
||||
"Keymaps do not create a focus system: move point normally to use local keys.\n"
|
||||
"Hint display and exact pointer shapes follow your Emacs settings and window system.")))
|
||||
151
examples/interaction-reference.el
Normal file
151
examples/interaction-reference.el
Normal file
@ -0,0 +1,151 @@
|
||||
;;; interaction-reference.el --- State and actions for the interaction lab -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
;; Loaded by the generic playground runner before interaction-reference.ebox.
|
||||
;; Each DSL evaluation creates its own state. Ebox supplies command/keymap
|
||||
;; construction, mouse-buffer targeting, and named-region updates.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ebox)
|
||||
(require 'cl-lib)
|
||||
(require 'seq)
|
||||
(require 'subr-x)
|
||||
|
||||
(cl-defstruct (interaction-demo
|
||||
(:constructor interaction-demo--make))
|
||||
"Application state belonging to one interaction-lab preview."
|
||||
(count 0) (step 1) (enabled t) warm events (serial 0) keymap subtract-keymap)
|
||||
|
||||
(defun interaction-demo-create ()
|
||||
"Create independent state and bindings for a new preview."
|
||||
(let ((demo (interaction-demo--make)))
|
||||
(setf (interaction-demo-keymap demo)
|
||||
(interaction-demo--step-map demo)
|
||||
(interaction-demo-subtract-keymap demo)
|
||||
(interaction-demo--step-map demo t))
|
||||
demo))
|
||||
|
||||
(defun interaction-demo-description (demo &optional subtract)
|
||||
"Return DEMO's current action hint, describing subtraction when SUBTRACT."
|
||||
(format "Live help: count = %d; this action %s %d. RET / SPC / click."
|
||||
(interaction-demo-count demo) (if subtract "subtracts" "adds")
|
||||
(interaction-demo-step demo)))
|
||||
|
||||
(defun interaction-demo--record (demo description)
|
||||
"Record DEMO's action DESCRIPTION and refresh its four-line event log."
|
||||
(push (format "%02d %s" (cl-incf (interaction-demo-serial demo))
|
||||
description)
|
||||
(interaction-demo-events demo))
|
||||
(setf (interaction-demo-events demo)
|
||||
(seq-take (interaction-demo-events demo) 4))
|
||||
(let ((events (interaction-demo-events demo)))
|
||||
(ebox-region-update
|
||||
"event-log" :content
|
||||
(string-join (append events (make-list (- 4 (length events)) "—")) "\n"))))
|
||||
|
||||
(defun interaction-demo--show-count (demo)
|
||||
"Refresh the displayed count for DEMO."
|
||||
(ebox-region-update "count" :content
|
||||
(format "COUNT %02d" (interaction-demo-count demo))))
|
||||
|
||||
(defun interaction-demo--add (demo amount)
|
||||
"Add AMOUNT to DEMO's count and record the action."
|
||||
(cl-incf (interaction-demo-count demo) amount)
|
||||
(interaction-demo--show-count demo)
|
||||
(interaction-demo--record
|
||||
demo (format "%+d → count %d" amount (interaction-demo-count demo))))
|
||||
|
||||
(defun interaction-demo-hover (demo)
|
||||
"Return DEMO's chosen hover palette using ordinary Ebox declarations."
|
||||
(if (interaction-demo-warm demo)
|
||||
'(:background-color "#F6D6AB" :color "#683D16"
|
||||
:text-decoration-line underline)
|
||||
'(:background-color "#BCDDE5" :color "#123E4B"
|
||||
:text-decoration-line underline)))
|
||||
|
||||
(defun interaction-demo--step-map (demo &optional subtract)
|
||||
"Create DEMO's captured step map, with SUBTRACT choosing negative activation."
|
||||
(let ((amount (interaction-demo-step demo)))
|
||||
(ebox-keymap-create
|
||||
:activate (lambda ()
|
||||
(interaction-demo--add demo (if subtract (- amount) amount)))
|
||||
:bindings
|
||||
(list (cons "+" (lambda () (interaction-demo--add demo amount)))
|
||||
(cons "-" (lambda () (interaction-demo--add demo (- amount))))))))
|
||||
|
||||
(defun interaction-demo--refresh (demo)
|
||||
"Publish DEMO's current action properties and binding summary."
|
||||
(let ((enabled (interaction-demo-enabled demo))
|
||||
(step (interaction-demo-step demo)))
|
||||
(ebox-region-update
|
||||
"live-action"
|
||||
:help-echo (and enabled
|
||||
(ebox-help-create
|
||||
(apply-partially #'interaction-demo-description demo)))
|
||||
:pointer (and enabled 'hand)
|
||||
:hover-style (and enabled (interaction-demo-hover demo))
|
||||
:keymap (and enabled (interaction-demo-keymap demo)))
|
||||
(ebox-region-update
|
||||
"action-label" :content
|
||||
(if enabled (format "ADD %d [+] " step) "BINDING REMOVED "))
|
||||
(ebox-region-update
|
||||
"subtract-text"
|
||||
:help-echo (and enabled
|
||||
(ebox-help-create
|
||||
(apply-partially #'interaction-demo-description demo t)))
|
||||
:keymap (and enabled (interaction-demo-subtract-keymap demo))))
|
||||
(ebox-region-update
|
||||
"binding-state" :content
|
||||
(format "step %d · %s · %s hover"
|
||||
(interaction-demo-step demo)
|
||||
(if (interaction-demo-enabled demo)
|
||||
"four properties attached" "four properties nil")
|
||||
(if (interaction-demo-warm demo) "warm" "cool"))))
|
||||
|
||||
(defun interaction-demo-action (demo action)
|
||||
"Return a native activation keymap for DEMO's named ACTION."
|
||||
(ebox-keymap-create
|
||||
:activate
|
||||
(lambda ()
|
||||
(pcase action
|
||||
('step
|
||||
(setf (interaction-demo-step demo)
|
||||
(if (= (interaction-demo-step demo) 1) 5 1)
|
||||
(interaction-demo-keymap demo)
|
||||
(interaction-demo--step-map demo)
|
||||
(interaction-demo-subtract-keymap demo)
|
||||
(interaction-demo--step-map demo t))
|
||||
(interaction-demo--refresh demo)
|
||||
(interaction-demo--record
|
||||
demo (format "Replaced keymap: step %d" (interaction-demo-step demo))))
|
||||
('binding
|
||||
(setf (interaction-demo-enabled demo)
|
||||
(not (interaction-demo-enabled demo)))
|
||||
(interaction-demo--refresh demo)
|
||||
(interaction-demo--record
|
||||
demo (if (interaction-demo-enabled demo)
|
||||
"Restored all four properties" "Removed all four properties")))
|
||||
('palette
|
||||
(setf (interaction-demo-warm demo)
|
||||
(not (interaction-demo-warm demo)))
|
||||
(interaction-demo--refresh demo)
|
||||
(interaction-demo--record
|
||||
demo (if (interaction-demo-warm demo)
|
||||
"Hover → warm palette" "Hover → cool palette")))
|
||||
('reset
|
||||
(setf (interaction-demo-count demo) 0)
|
||||
(interaction-demo--show-count demo)
|
||||
(interaction-demo--record demo "Reset count"))
|
||||
('group
|
||||
(ebox-update-selector (current-buffer) ".demo-status"
|
||||
:content "READY" :color "#287A52"
|
||||
:help-echo "This status was updated with its class group.")
|
||||
(interaction-demo--record demo "Updated three .demo-status regions in one batch"))
|
||||
('parent (interaction-demo--record demo "PARENT surface activated"))
|
||||
('child (interaction-demo--record demo "CHILD text activated"))
|
||||
(_ (error "Unknown interaction-lab action: %S" action))))))
|
||||
|
||||
(provide 'interaction-demo-reference)
|
||||
|
||||
;;; interaction-reference.el ends here
|
||||
9
examples/layer-minimal.ebox
Normal file
9
examples/layer-minimal.ebox
Normal file
@ -0,0 +1,9 @@
|
||||
'(box :width (ch 32) :height (lh 5)
|
||||
:background-color "#233044"
|
||||
(box :color "#cbd5e1"
|
||||
"01234567890123456789012345678901\nabcdefghijklmnopqrstuvwxyzABCDEF\n01234567890123456789012345678901\nabcdefghijklmnopqrstuvwxyzABCDEF\n01234567890123456789012345678901")
|
||||
(box :position absolute :left (ch 6) :top (lh 1)
|
||||
:width (ch 18) :height (lh 3) :z-index 1
|
||||
:background-color "#2563eb" :color "white"
|
||||
:padding-inline (ch 1)
|
||||
"Upper layer\nCovers the base\nBase stays alive"))
|
||||
246
examples/layer-reference.ebox
Normal file
246
examples/layer-reference.ebox
Normal file
@ -0,0 +1,246 @@
|
||||
;; Layout data only. The matching companion owns state and business actions.
|
||||
(let ((demo (layer-demo-create)))
|
||||
`(column
|
||||
:width (vw 100) :height (vh 100)
|
||||
:padding ((lh 1) (ch 2))
|
||||
:background-color "#101117" :color "#DCE0EC"
|
||||
(flex :flex-wrap wrap :gap ((lh 0) (ch 2)) :padding-bottom (lh 1)
|
||||
(box :padding-inline (ch 1) :background-color "#D7FA73"
|
||||
:color "#151A0C" :font-weight bold "ORBIT / CREATIVE OS")
|
||||
(box :color "#A99BFF" "VOL. 007 / SPATIAL EDITION"))
|
||||
(box :padding ((lh 1) (ch 2)) :background-color "#1A1D28"
|
||||
(box :font-weight bold :color "#F7F8FF"
|
||||
"MAKE ROOM\nFOR THE UNEXPECTED.")
|
||||
(box :padding-top (lh 1) :color "#AEB6CE"
|
||||
"A desktop of intersecting ideas.\nPull it apart. Shift the focus. Change the atmosphere."))
|
||||
(flex :flex-wrap wrap :gap ((lh 1) (ch 1)) :padding-block (lh 1)
|
||||
:pointer hand :font-weight bold :color "#101117"
|
||||
:hover-style (:background-color "#FFFFFF")
|
||||
(box :padding-inline (ch 1) :background-color "#D7FA73"
|
||||
:help-echo "Separate the three cards, then bring them back into their original stack."
|
||||
:keymap ,(layer-demo-action demo 'explode) "EXPLODE STACK")
|
||||
(box :padding-inline (ch 1) :background-color "#A99BFF"
|
||||
:help-echo "Bring Studio, Next, and Signal to the front in turn."
|
||||
:keymap ,(layer-demo-action demo 'focus) "CYCLE FOCUS")
|
||||
(box :padding-inline (ch 1) :background-color "#B9E9F2"
|
||||
:help-echo "Switch between electric violet / ice / lime and a warmer studio palette."
|
||||
:keymap ,(layer-demo-action demo 'mood) "SWITCH MOOD"))
|
||||
|
||||
;; Each card is a local stacking group. The dark slabs are separate
|
||||
;; backplates; moving the cards deliberately reveals the composition.
|
||||
(box :id "orbit-canvas" :height (lh 25) :background-color "#101117"
|
||||
(box :position absolute :left (ch 1) :top (lh 0)
|
||||
:width (ch 24) :height (lh 1) :color "#9099B4"
|
||||
"FIELD / 007 + +")
|
||||
(box :position absolute :left (% 75) :top (lh 0)
|
||||
:width (ch 18) :height (lh 1) :color "#D7FA73"
|
||||
"COMPOSITION / LIVE")
|
||||
(box :position absolute :left (ch 0) :top (lh 2)
|
||||
:width (ch 2) :height (lh 21) :color "#4E566E"
|
||||
"00\n |\n |\n |\n04\n |\n |\n |\n08\n |\n |\n |\n12\n |\n |\n |\n16\n |\n |\n |\n20")
|
||||
(box :position absolute :left (ch 8) :top (lh 5)
|
||||
:width (ch 68) :height (lh 16) :background-color "#32354B"
|
||||
(box :position absolute :left (ch 2) :top (lh 1)
|
||||
:width (ch 64) :height (lh 14) :background-color "#202333"
|
||||
(box :position absolute :left (ch 1) :top (lh 12)
|
||||
:width (ch 32) :height (lh 1) :color "#8B92AE"
|
||||
"+ LOCAL SPACE / INFINITE IDEAS")))
|
||||
(box :position absolute :left (ch 6) :top (lh 4)
|
||||
:width (ch 40) :height (lh 12) :z-index 1
|
||||
:background-color "#06070B")
|
||||
(box :position absolute :left (ch 48) :top (lh 3)
|
||||
:width (ch 28) :height (lh 9) :z-index 1
|
||||
:background-color "#06070B")
|
||||
(box :position absolute :left (ch 22) :top (lh 8)
|
||||
:width (ch 40) :height (lh 13) :z-index 1
|
||||
:background-color "#06070B")
|
||||
|
||||
(box :id "orbit-violet" :position absolute :left (ch 4) :top (lh 3)
|
||||
:width (ch 40) :height (lh 12) :z-index 2
|
||||
:padding-inline (ch 2) :background-color "#A99BFF" :color "#201A38"
|
||||
:help-echo "Studio is a local group: its small lime badge cannot escape above a higher sibling card."
|
||||
(box :padding-top (lh 1) :font-weight bold "STUDIO / 01")
|
||||
(box :padding-top (lh 1) "FORM FOLLOWS\nIMAGINATION.")
|
||||
(box :position absolute :left (ch 2) :top (lh 5)
|
||||
:width (ch 32) :height (lh 5) :background-color "#30284E"
|
||||
:color "#D4C9FF"
|
||||
(box :position absolute :left (ch 2) :top (lh 1)
|
||||
:width (ch 14) :height (lh 3) :background-color "#645293"
|
||||
" [ [ + ] ]\n\n SHAPE / PLAY")
|
||||
(box :position absolute :left (ch 12) :top (lh 0)
|
||||
:width (ch 12) :height (lh 3) :z-index 1
|
||||
:background-color "#D7CFFF" :color "#30284E"
|
||||
" + +\n / /\n + +")
|
||||
(box :position absolute :left (ch 20) :top (lh 2)
|
||||
:width (ch 10) :height (lh 2) :z-index 60
|
||||
:background-color "#D7FA73" :color "#202816"
|
||||
:text-align center "DEPTH\n03"))
|
||||
(box :position absolute :left (ch 2) :top (lh 10)
|
||||
:width (ch 30) :height (lh 1) "OBJECT 01 / EDITION 007"))
|
||||
|
||||
(box :id "orbit-lime" :position absolute :left (ch 46) :top (lh 2)
|
||||
:width (ch 28) :height (lh 9) :z-index 3
|
||||
:padding-inline (ch 2) :background-color "#D7FA73" :color "#202816"
|
||||
:help-echo "Next is a separate sibling card. Cycle Focus to move its whole local composition forward."
|
||||
(box :padding-top (lh 1) :font-weight bold "NEXT / 02")
|
||||
(box :padding-top (lh 1) "LESS NOISE.\nMORE POSSIBILITY.")
|
||||
(box :position absolute :left (ch 15) :top (lh 5)
|
||||
:width (ch 9) :height (lh 3) :background-color "#24301A"
|
||||
:color "#D7FA73" :text-align center " /\n /___\n / ")
|
||||
(box :position absolute :left (ch 2) :top (lh 6)
|
||||
:width (ch 12) :height (lh 2) "OPEN SPACE\n24 / 7"))
|
||||
|
||||
(box :id "orbit-signal" :position absolute :left (ch 20) :top (lh 7)
|
||||
:width (ch 40) :height (lh 13) :z-index 4
|
||||
:padding-inline (ch 2) :background-color "#B9E9F2" :color "#173139"
|
||||
:help-echo "Signal starts in front. Its bars, labels, and floating badge move together as one local stack."
|
||||
(box :padding-top (lh 1) :font-weight bold "SIGNAL / NOW PLAYING")
|
||||
(box :padding-top (lh 1) "AFTER HOURS\nA soundtrack for what comes next.")
|
||||
(box :position absolute :left (ch 2) :top (lh 5)
|
||||
:width (ch 32) :height (lh 5) :background-color "#173139"
|
||||
:color "#B9E9F2"
|
||||
(box :position absolute :left (ch 2) :top (lh 3)
|
||||
:width (ch 3) :height (lh 1) :background-color "#B9E9F2")
|
||||
(box :position absolute :left (ch 6) :top (lh 2)
|
||||
:width (ch 3) :height (lh 2) :background-color "#A99BFF")
|
||||
(box :position absolute :left (ch 10) :top (lh 1)
|
||||
:width (ch 3) :height (lh 3) :background-color "#D7FA73")
|
||||
(box :position absolute :left (ch 14) :top (lh 0)
|
||||
:width (ch 3) :height (lh 4) :background-color "#B9E9F2")
|
||||
(box :position absolute :left (ch 18) :top (lh 2)
|
||||
:width (ch 3) :height (lh 2) :background-color "#A99BFF")
|
||||
(box :position absolute :left (ch 22) :top (lh 1)
|
||||
:width (ch 3) :height (lh 3) :background-color "#D7FA73")
|
||||
(box :position absolute :left (ch 26) :top (lh 3)
|
||||
:width (ch 3) :height (lh 1) :background-color "#B9E9F2")
|
||||
(box :position absolute :left (ch 20) :top (lh 0)
|
||||
:width (ch 10) :height (lh 1) :z-index 8
|
||||
:background-color "#F9FCF3" :color "#173139"
|
||||
:text-align center "ON AIR +"))
|
||||
(box :position absolute :left (ch 2) :top (lh 11)
|
||||
:width (ch 32) :height (lh 1) "03:42 ========---- 05:18"))
|
||||
|
||||
(box :position absolute :left (ch 61) :top (lh 15)
|
||||
:width (ch 15) :height (lh 3) :z-index 5
|
||||
:padding-inline (ch 1) :background-color "#F6F5EF" :color "#282B35"
|
||||
"+ IN SYNC\n3 OPEN IDEAS\nSHARED SPACE")
|
||||
(box :position absolute :left (ch 3) :top (lh 22)
|
||||
:width (ch 72) :height (lh 1) :color "#A99BFF"
|
||||
"+ . + . + . + . + . + . + . + . + . + . + .")
|
||||
(box :position absolute :left (ch 3) :top (lh 24)
|
||||
:width (ch 70) :height (lh 1) :color "#9099B4"
|
||||
"ORBIT / 007 COMPOSE IN LAYERS. THINK IN POSSIBILITIES."))
|
||||
(box :id "orbit-state" :padding-inline (ch 1)
|
||||
:background-color "#222637" :color "#D7FA73"
|
||||
,(layer-demo-orbit-status demo))
|
||||
(box :padding-top (lh 2) :font-weight bold :color "#F7F8FF"
|
||||
"EBOX / LAYER LAB")
|
||||
(box :color "#AEB6CE" "Retained content, opaque overlap, native interaction.")
|
||||
(box :padding-block (lh 1)
|
||||
"Click a control, or move point into it and press RET / SPC.\n"
|
||||
"All text shares one font size. Scroll with the wheel or C-v when needed.")
|
||||
|
||||
(box "01 / LAYERED DESK absolute position + local stacking")
|
||||
(flex :flex-wrap wrap :gap ((lh 1) (ch 1)) :padding-block (lh 1)
|
||||
:pointer hand :hover-style (:background-color "#505B7A")
|
||||
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||
:help-echo "Hide or show Panel A. Revealing a lower layer shows its latest content."
|
||||
:keymap ,(layer-demo-action demo 'toggle) "TOGGLE PANEL A")
|
||||
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||
:help-echo "Move Panel A between left 4ch / top 2lh and left 10ch / top 0lh."
|
||||
:keymap ,(layer-demo-action demo 'move) "MOVE PANEL A")
|
||||
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||
:help-echo "Switch Panel A between z=2 and z=4; Panel B stays at z=3."
|
||||
:keymap ,(layer-demo-action demo 'raise) "RAISE / LOWER A")
|
||||
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||
:help-echo "Update the lower note even when every part of it is covered."
|
||||
:keymap ,(layer-demo-action demo 'update-lower) "UPDATE COVERED NOTE"))
|
||||
(box :id "layer-desk" :height (lh 10) :background-color "#222637"
|
||||
(box :height (lh 10)
|
||||
"DESK / ordinary content stays in its flow slot\n"
|
||||
"........................................................................\n"
|
||||
"........................................................................\n"
|
||||
"........................................................................\n"
|
||||
"........................................................................\n"
|
||||
"........................................................................\n"
|
||||
"........................................................................\n"
|
||||
"........................................................................\n"
|
||||
"........................................................................\n"
|
||||
"DESK END / panels do not add flow rows")
|
||||
(box :id "lower-note" :position absolute :left (ch 4) :top (lh 2)
|
||||
:width (ch 38) :height (lh 5) :z-index 0 :padding-inline (ch 1)
|
||||
:background-color "#C9DED0" :color "#244F36" :pointer hand
|
||||
:hover-style (:background-color "#ADCAB7")
|
||||
:help-echo ,(ebox-help-create
|
||||
(apply-partially #'layer-demo-help demo 'lower))
|
||||
:keymap ,(layer-demo-action demo 'lower)
|
||||
,(layer-demo-lower-content demo))
|
||||
(box :id "panel-a" :position absolute :left (ch 4) :top (lh 2)
|
||||
:width (ch 38) :height (lh 5) :z-index 2 :padding-inline (ch 1)
|
||||
:background-color "#3A687E" :color "#FFFFFF"
|
||||
:help-echo "Panel A is opaque, including its blank padding. Use the toolbar to move or raise it."
|
||||
"PANEL A / blue\nMove and raise from the toolbar.\nA survives under the amber panel.\nOpaque padding covers too.\nThe covered note remains alive.")
|
||||
(box :id "panel-b" :position absolute :left (ch 22) :top (lh 4)
|
||||
:width (ch 28) :height (lh 4) :z-index 3 :padding-inline (ch 1)
|
||||
:background-color "#D7AF6D" :color "#503B21"
|
||||
:help-echo "Panel B stays at z=3. Raising A changes which panel owns the overlap."
|
||||
"PANEL B / amber\nFixed at z=3.\nLater layers cover earlier\npaint, not logical data."))
|
||||
(box :height (lh 1))
|
||||
|
||||
(box "02 / RELATIVE POSITION reserve the slot, shift the paint")
|
||||
(column :height (lh 4) :background-color "#292437"
|
||||
(box :id "relative-marker" :position relative :left (ch 4) :top (lh 1)
|
||||
:width (ch 24) :height (lh 1) :z-index 1
|
||||
:background-color "#765D87" :color "#FFFFFF" "RELATIVE / +4ch, +1lh")
|
||||
(box "flow row 2: the shifted marker covers part of this row")
|
||||
(box :id "relative-following" "FLOW ROW 3 / the original slot was kept")
|
||||
(box "Each parent keeps a local stacking group automatically."))
|
||||
(box :height (lh 1))
|
||||
|
||||
(box "03 / ANCHORED MENU nested owner + root projection")
|
||||
(box :padding-block (lh 1)
|
||||
"The owner below clips to one row. Its menu stays a logical child\n"
|
||||
"but paints in the root layer, outside that local clip.")
|
||||
(flex :gap ((lh 0) (ch 1)) :padding-bottom (lh 1) :pointer hand
|
||||
:hover-style (:background-color "#505B7A")
|
||||
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||
:help-echo "Cycle bottom-start, bottom-end, top-start, and top-end. Open the menu to compare."
|
||||
:keymap ,(layer-demo-action demo 'placement) "CYCLE PLACEMENT")
|
||||
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||
:help-echo "Move the relative trigger from left 4ch to 12ch. The open menu follows its anchor."
|
||||
:keymap ,(layer-demo-action demo 'anchor) "MOVE ANCHOR"))
|
||||
(box :height (lh 3) :background-color "#1C2430"
|
||||
"SPACE ABOVE / top placements can cover this area\n"
|
||||
"........................................................................\n"
|
||||
"........................................................................")
|
||||
(box :id "menu-owner" :height (lh 1) :overflow hidden :background-color "#303D43"
|
||||
(box :id "menu-trigger" :position relative :left (ch 4) :top (lh 0)
|
||||
:width (ch 32) :height (lh 1) :padding-inline (ch 1)
|
||||
:background-color "#286347" :color "#FFFFFF" :pointer hand
|
||||
:hover-style (:background-color "#407E5C")
|
||||
:help-echo ,(ebox-help-create
|
||||
(apply-partially #'layer-demo-help demo 'menu))
|
||||
:keymap ,(layer-demo-action demo 'menu) "OPEN / CLOSE ROOT MENU")
|
||||
(box :id "root-menu" :position absolute :layer root :anchor "menu-trigger"
|
||||
:placement bottom-start :z-index 50 :visibility hidden
|
||||
:width (ch 26) :height (lh 3) :background-color "#153F4A" :color "#FFFFFF"
|
||||
(box :padding-inline (ch 1) "ROOT MENU / choose")
|
||||
(box :id "menu-sage" :padding-inline (ch 1) :pointer hand
|
||||
:hover-style (:background-color "#407E5C")
|
||||
:help-echo "Choose sage, update the result, and close the menu."
|
||||
:keymap ,(layer-demo-action demo 'sage) "CHOOSE SAGE")
|
||||
(box :id "menu-amber" :padding-inline (ch 1) :pointer hand
|
||||
:hover-style (:background-color "#895314")
|
||||
:help-echo "Choose amber, update the result, and close the menu."
|
||||
:keymap ,(layer-demo-action demo 'amber) "CHOOSE AMBER")))
|
||||
(box :height (lh 3) :background-color "#1C2430"
|
||||
"SPACE BELOW / bottom placements can cover this area\n"
|
||||
"........................................................................\n"
|
||||
"........................................................................")
|
||||
(box :id "menu-result" :padding-block (lh 1) "No menu selection yet.")
|
||||
(box :id "layer-status" :background-color "#153F4A" :color "#FFFFFF"
|
||||
:padding-inline (ch 1) ,(layer-demo-status demo))
|
||||
(box :padding-top (lh 1)
|
||||
"Try: update the covered note twice, hide A, then activate the revealed note.\n"
|
||||
"Open the menu, cycle all four placements, move its anchor, and select a color.\n"
|
||||
"Commands publish named-region updates. Layers do not create a focus system.")))
|
||||
134
examples/layer-reference.el
Normal file
134
examples/layer-reference.el
Normal file
@ -0,0 +1,134 @@
|
||||
;;; layer-reference.el --- State and actions for the layer lab -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
;; The matching .ebox file owns every layout declaration. Each evaluation
|
||||
;; creates independent application state; public Ebox adapters dispatch native
|
||||
;; commands and help in the preview buffer and update semantic region IDs.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'ebox)
|
||||
|
||||
(cl-defstruct (layer-demo (:constructor layer-demo-create))
|
||||
"Application state belonging to one layer-lab preview."
|
||||
(visible t) moved raised (revision 0) (lower-clicks 0)
|
||||
menu-visible (placement 0) anchor-moved (selection "none")
|
||||
exploded (focus 2) warm)
|
||||
|
||||
(defun layer-demo-orbit-status (demo)
|
||||
"Describe the spatial desktop controls for DEMO."
|
||||
(format "%s / %s IN FRONT / %s PALETTE"
|
||||
(if (layer-demo-exploded demo) "EXPANDED" "STACKED")
|
||||
(aref ["STUDIO" "NEXT" "SIGNAL"] (layer-demo-focus demo))
|
||||
(if (layer-demo-warm demo) "SOLAR" "LUNAR")))
|
||||
|
||||
(defconst layer-demo-placements
|
||||
[bottom-start bottom-end top-start top-end]
|
||||
"Anchor placements visited by the lab's placement control.")
|
||||
|
||||
(defun layer-demo-lower-content (demo)
|
||||
"Return the covered note's current content for DEMO."
|
||||
(format "LOWER REV %02d\nUpdated even while covered.\nReveal me, then click this surface.\nLower activations: %02d\nThe logical note never left the tree."
|
||||
(layer-demo-revision demo) (layer-demo-lower-clicks demo)))
|
||||
|
||||
(defun layer-demo-status (demo)
|
||||
"Describe DEMO's currently published panel and menu state."
|
||||
(format "A: %s / %s / z=%d | lower rev=%02d | menu: %s / %s | choice: %s"
|
||||
(if (layer-demo-visible demo) "shown" "hidden")
|
||||
(if (layer-demo-moved demo) "moved" "home")
|
||||
(if (layer-demo-raised demo) 4 2)
|
||||
(layer-demo-revision demo)
|
||||
(if (layer-demo-menu-visible demo) "open" "closed")
|
||||
(aref layer-demo-placements (layer-demo-placement demo))
|
||||
(layer-demo-selection demo)))
|
||||
|
||||
(defun layer-demo-help (demo subject)
|
||||
"Return live native help for DEMO's SUBJECT."
|
||||
(pcase subject
|
||||
('lower (format "Lower note revision %d; activated %d times. RET / SPC / click."
|
||||
(layer-demo-revision demo) (layer-demo-lower-clicks demo)))
|
||||
('menu (format "Menu is %s; placement %s; selected %s. RET / SPC / click."
|
||||
(if (layer-demo-menu-visible demo) "open" "closed")
|
||||
(aref layer-demo-placements (layer-demo-placement demo))
|
||||
(layer-demo-selection demo)))
|
||||
(_ (layer-demo-status demo))))
|
||||
|
||||
(defun layer-demo-action (demo action)
|
||||
"Create a native keymap for DEMO's business ACTION."
|
||||
(ebox-keymap-create
|
||||
:activate
|
||||
(lambda ()
|
||||
(pcase action
|
||||
('explode
|
||||
(setf (layer-demo-exploded demo) (not (layer-demo-exploded demo)))
|
||||
(let ((expanded (layer-demo-exploded demo)))
|
||||
(ebox-region-update "orbit-violet"
|
||||
:left (if expanded '(ch 2) '(ch 4))
|
||||
:top (if expanded '(lh 1) '(lh 3)))
|
||||
(ebox-region-update "orbit-signal"
|
||||
:left (if expanded '(ch 26) '(ch 20))
|
||||
:top (if expanded '(lh 11) '(lh 7)))
|
||||
(ebox-region-update "orbit-lime"
|
||||
:left (if expanded '(ch 52) '(ch 46))
|
||||
:top (if expanded '(lh 0) '(lh 2)))))
|
||||
('focus
|
||||
(setf (layer-demo-focus demo) (% (1+ (layer-demo-focus demo)) 3))
|
||||
(cl-loop for id in '("orbit-violet" "orbit-lime" "orbit-signal")
|
||||
for index from 0
|
||||
do (ebox-region-update id :z-index
|
||||
(if (= index (layer-demo-focus demo))
|
||||
6 (+ index 2)))))
|
||||
('mood
|
||||
(setf (layer-demo-warm demo) (not (layer-demo-warm demo)))
|
||||
(cl-loop for id in '("orbit-violet" "orbit-signal" "orbit-lime")
|
||||
for color in (if (layer-demo-warm demo)
|
||||
'("#FFADCF" "#FFD49B" "#B8F1CD")
|
||||
'("#A99BFF" "#B9E9F2" "#D7FA73"))
|
||||
do (ebox-region-update id :background-color color)))
|
||||
('toggle
|
||||
(setf (layer-demo-visible demo) (not (layer-demo-visible demo)))
|
||||
(ebox-region-update "panel-a" :visibility
|
||||
(if (layer-demo-visible demo) 'visible 'hidden)))
|
||||
('move
|
||||
(setf (layer-demo-moved demo) (not (layer-demo-moved demo)))
|
||||
(ebox-region-update "panel-a"
|
||||
:left (if (layer-demo-moved demo) '(ch 10) '(ch 4))
|
||||
:top (if (layer-demo-moved demo) '(lh 0) '(lh 2))))
|
||||
('raise
|
||||
(setf (layer-demo-raised demo) (not (layer-demo-raised demo)))
|
||||
(ebox-region-update "panel-a" :z-index
|
||||
(if (layer-demo-raised demo) 4 2)))
|
||||
('update-lower
|
||||
(cl-incf (layer-demo-revision demo))
|
||||
(ebox-region-update "lower-note" :content (layer-demo-lower-content demo)))
|
||||
('lower
|
||||
(cl-incf (layer-demo-lower-clicks demo))
|
||||
(ebox-region-update "lower-note" :content (layer-demo-lower-content demo)))
|
||||
('menu
|
||||
(setf (layer-demo-menu-visible demo) (not (layer-demo-menu-visible demo)))
|
||||
(ebox-region-update "root-menu" :visibility
|
||||
(if (layer-demo-menu-visible demo) 'visible 'hidden)))
|
||||
('placement
|
||||
(setf (layer-demo-placement demo) (% (1+ (layer-demo-placement demo)) 4))
|
||||
(ebox-region-update "root-menu" :placement
|
||||
(aref layer-demo-placements (layer-demo-placement demo))))
|
||||
('anchor
|
||||
(setf (layer-demo-anchor-moved demo) (not (layer-demo-anchor-moved demo)))
|
||||
(ebox-region-update "menu-trigger" :left
|
||||
(if (layer-demo-anchor-moved demo) '(ch 12) '(ch 4))))
|
||||
((or 'sage 'amber)
|
||||
(setf (layer-demo-selection demo) (symbol-name action)
|
||||
(layer-demo-menu-visible demo) nil)
|
||||
(ebox-region-update "menu-result" :content
|
||||
(format "Selected %s from the anchored menu." action)
|
||||
:color (if (eq action 'sage) "#286347" "#895314"))
|
||||
(ebox-region-update "root-menu" :visibility 'hidden))
|
||||
(_ (error "Unknown layer-lab action: %S" action)))
|
||||
(if (memq action '(explode focus mood))
|
||||
(ebox-region-update "orbit-state" :content (layer-demo-orbit-status demo))
|
||||
(ebox-region-update "layer-status" :content (layer-demo-status demo))))))
|
||||
|
||||
(provide 'layer-demo-reference)
|
||||
|
||||
;;; layer-reference.el ends here
|
||||
@ -1,61 +0,0 @@
|
||||
(column
|
||||
(box :width '(720) :padding '(1 (18)) :border "#8F432F" :color
|
||||
"#252A2E" :bgcolor "#FFFDF8" :font-weight 'bold :text-align 'center
|
||||
"Ebox Playground\nPixel-precise layout, composed only from public nodes.")
|
||||
(box :width '(720) :height 1 :bgcolor "#FAF7F0")
|
||||
(box :width '(720) :padding '(0 (16)) :border "#C97252"
|
||||
:border-bottom 'none :color "#252A2E" :bgcolor "#F1D4C9"
|
||||
:wrap-mode 'word
|
||||
"01 FIXED + FRACTIONAL\nA definite Grid width divides into one exact track and one flexible track.")
|
||||
(grid :width '(720) :grid-template-columns '((220) 1fr) :gap
|
||||
'(1 (12)) :border "#C97252" :bgcolor "#FFF9F5"
|
||||
(box :width '(220) :padding '(1 0) :text-align 'center :color
|
||||
"#252A2E" :bgcolor "#F1D4C9"
|
||||
"FIXED TRACK\n220 px stays exact")
|
||||
(box :width '(486) :padding '(1 0) :text-align 'center :color
|
||||
"#252A2E" :bgcolor "#F9E8E0"
|
||||
"FRACTIONAL TRACK\n1fr receives the remaining width")
|
||||
(box :grid-column '(1 :span 2) :width '(718) :padding '(1 0)
|
||||
:color "#663322" :bgcolor "#FFFDF8" :text-align 'center
|
||||
:wrap-mode 'word
|
||||
"The Grid owns one definite width; its tracks divide that space without competing viewport calculations."))
|
||||
(box :width '(720) :height 1 :bgcolor "#FAF7F0")
|
||||
(box :width '(720) :padding '(0 (16)) :border "#6D8A73"
|
||||
:border-bottom 'none :color "#252A2E" :bgcolor "#DCEBDD"
|
||||
:wrap-mode 'word
|
||||
"02 EXPLICIT PLACEMENT\nSpan columns, target cells, and leave the remaining nodes to auto placement.")
|
||||
(grid :width '(720) :grid-template-columns '((210) (210) 1fr) :gap
|
||||
'(1 (10)) :border "#6D8A73" :bgcolor "#F7FBF7"
|
||||
(box :grid-column '(1 :span 2) :width '(430) :padding '(1 0)
|
||||
:text-align 'center :color "#24422D" :bgcolor "#DCEBDD"
|
||||
"SPAN 1 → 3")
|
||||
(box :grid-column 3 :grid-row 1 :width '(278) :padding '(1 0)
|
||||
:text-align 'center :color "#24422D" :bgcolor "#C8DDCB"
|
||||
"COLUMN 3")
|
||||
(box :grid-column 1 :grid-row 2 :width '(210) :padding '(1 0)
|
||||
:text-align 'center :color "#24422D" :bgcolor "#EDF5EE"
|
||||
"ROW 2 / COL 1")
|
||||
(box :width '(210) :padding '(1 0) :text-align 'center :color
|
||||
"#24422D" :bgcolor "#E3F0E5" "AUTO PLACED")
|
||||
(box :width '(278) :padding '(1 0) :text-align 'center :color
|
||||
"#24422D" :bgcolor "#D4E6D7" "STABLE CELLS"))
|
||||
(box :width '(720) :height 1 :bgcolor "#FAF7F0")
|
||||
(box :width '(720) :padding '(0 (16)) :border "#4E7890"
|
||||
:border-bottom 'none :color "#252A2E" :bgcolor "#D9EAF2"
|
||||
:wrap-mode 'word
|
||||
"03 PUBLIC COMPOSITION\nColumn, Flex, and Grid share the same public node contract.")
|
||||
(flex :width '(720) :flex-flow '(row wrap) :gap '(1 (12)) :padding
|
||||
'(1 (12)) :border "#4E7890" :bgcolor "#F7FAFC"
|
||||
(box :flex-grow 1 :flex-shrink 1 :flex-basis '(200) :min-width
|
||||
'(180) :padding '(1 (12)) :color "#193846" :bgcolor
|
||||
"#D9EAF2" "COLUMN\nvertical composition")
|
||||
(box :flex-grow 1 :flex-shrink 1 :flex-basis '(200) :min-width
|
||||
'(180) :padding '(1 (12)) :color "#3F3655" :bgcolor
|
||||
"#E7E2F1" "FLEX\nwrap and distribute")
|
||||
(box :flex-grow 1 :flex-shrink 1 :flex-basis '(200) :min-width
|
||||
'(180) :padding '(1 (12)) :color "#6B3020" :bgcolor
|
||||
"#F1D4C9" "GRID\nplace in two dimensions"))
|
||||
(box :width '(720) :height 1 :bgcolor "#FAF7F0")
|
||||
(box :width '(720) :padding '(0 (16)) :border "#8D887F" :color
|
||||
"#4D5651" :bgcolor "#EEEAE2" :wrap-mode 'word
|
||||
"AUTHOR FORMS String · text · box · row · column · flex · grid"))
|
||||
@ -1,57 +0,0 @@
|
||||
(column :margin-left 1 :margin-top 1 :padding '(1 2 1 2) :bgcolor
|
||||
"linen"
|
||||
(box :padding '(1 (18)) :border "#334155" :text-align 'center
|
||||
:color "#000" :bgcolor "ghost white"
|
||||
"Ebox Responsive Layout\nDrag the preview window: sections stretch to the current viewport, cards reflow, and text wraps without changing the window layout.")
|
||||
(box :height 1)
|
||||
(box :padding '(0 (16)) :border "#0F766E" :bgcolor "#F0FDFA"
|
||||
:color "#134E4A" :wrap-mode 'word
|
||||
"No explicit viewport width here\nThis page mostly omits :width. The Playground binds the current preview width while rendering, so auto width behaves like a responsive container.")
|
||||
(box :height 1)
|
||||
(box :padding '(0 (16)) :border "#EA580C" :border-bottom 'none
|
||||
:bgcolor "#FFF7ED" :color "#431407" :wrap-mode 'word
|
||||
"Adaptive toolbar\nButtons below keep stable sizes. The row packs as many as fit and then wraps.")
|
||||
(flex :flex-flow '(row wrap) :column-gap '(8) :row-gap 1
|
||||
:padding '(1 (8)) :border "#FDBA74" :bgcolor "#FFFBF5"
|
||||
(box :width '(96) :padding '(0 (8)) :border "#EA580C"
|
||||
:bgcolor "#FED7AA" :color "#111827" :text-align
|
||||
'center "Render")
|
||||
(box :width '(88) :padding '(0 (8)) :border "#D97706"
|
||||
:bgcolor "#FEF3C7" :color "#111827" :text-align
|
||||
'center "Save")
|
||||
(box :width '(96) :padding '(0 (8)) :border "#16A34A"
|
||||
:bgcolor "#DCFCE7" :color "#14532D" :text-align
|
||||
'center "Format")
|
||||
(box :width '(104) :padding '(0 (8)) :border "#0284C7"
|
||||
:bgcolor "#E0F2FE" :color "#0C4A6E" :text-align
|
||||
'center "Sample")
|
||||
(box :width '(88) :padding '(0 (8)) :border "#9333EA"
|
||||
:bgcolor "#F3E8FF" :color "#3B0764" :text-align
|
||||
'center "Write"))
|
||||
(box :height 1)
|
||||
(box :padding '(0 (16)) :border "#2563EB" :border-bottom 'none
|
||||
:bgcolor "#EFF6FF" :color "#1E3A8A" :wrap-mode 'word
|
||||
"Responsive cards\nEach card starts from a flex-basis, grows when space is available, and wraps to new rows when the preview becomes narrow.")
|
||||
(flex :flex-flow '(row wrap) :column-gap '(12) :row-gap 1
|
||||
:padding '(1 (8)) :border "#93C5FD" :bgcolor "#F8FBFF"
|
||||
:justify-content 'center
|
||||
(box :flex '(1 1 (210)) :min-width '(180) :max-width
|
||||
'(280) :padding '(0 (10)) :bgcolor "#DBEAFE" :color
|
||||
"#111827" :wrap-mode 'word
|
||||
"Overview\nThe row uses the current preview width automatically.")
|
||||
(box :flex '(1 1 (210)) :min-width '(180) :max-width
|
||||
'(300) :padding '(0 (10)) :bgcolor "#E0E7FF" :color
|
||||
"#111827" :wrap-mode 'word
|
||||
"Editor\nDrag slowly or quickly; resize rendering follows the viewport.")
|
||||
(box :flex '(1 1 (210)) :min-width '(180) :max-width
|
||||
'(320) :padding '(0 (10)) :bgcolor "#EDE9FE" :color
|
||||
"#111827" :wrap-mode 'word
|
||||
"Preview\nThe preview updates from the current viewport width.")
|
||||
(box :flex '(1 1 (210)) :min-width '(180) :max-width
|
||||
'(280) :padding '(0 (10)) :bgcolor "#CFFAFE" :color
|
||||
"#164E63" :wrap-mode 'word
|
||||
"Flow\nCards keep readable widths instead of overflowing."))
|
||||
(box :height 1)
|
||||
(box :padding '(0 (16)) :border "#475569" :bgcolor "#F8FAFC"
|
||||
:color "#0F172A" :wrap-mode 'word
|
||||
"What to watch\nNarrow preview: cards stack and text wraps. Wide preview: cards share a row and expand up to max-width. The source/preview split is preserved."))
|
||||
433
examples/size-reference.ebox
Normal file
433
examples/size-reference.ebox
Normal file
@ -0,0 +1,433 @@
|
||||
'(column :id "size-root" :width (vw 100) :height (vh 100) :overflow scroll
|
||||
:bgcolor "#FAF7F0" :color "#253038"
|
||||
(box :padding-inline (px 6) :border "#2E3740" :bgcolor "#FFFDF8"
|
||||
:font-weight bold :text-align center "Ebox Size Reference")
|
||||
(box :padding-inline (px 6) :border-left "#2E3740"
|
||||
:border-right "#2E3740" :border-bottom "#2E3740" :bgcolor "#FFFDF8"
|
||||
:text-align center "Lengths, sizing keywords, and calculations.\nHorizontal: px/ch/vw/%. Vertical: lh/vh/%.\nRead the solid rectangle: its edges show the measured Box width and height, including empty space.\nWhite bordered cards hold labels; they are not the measured Box.")
|
||||
(box :height (lh 1))
|
||||
|
||||
;; Each regular card has 220 px of content and a one-pixel border.
|
||||
;; Stretch aligns outer frames; the colored targets retain their own sizes.
|
||||
(box :padding-inline (px 6) :border "#B85C3C" :bgcolor "#F1D4C9"
|
||||
"01 / LENGTH UNITS\nEach card has a 220 px measuring surface.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#B85C3C" :border-right "#B85C3C"
|
||||
:border-bottom "#B85C3C" :bgcolor "#FFF9F4"
|
||||
(column :width (px 222) :border "#B85C3C" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||
":width (px 180)\nPX / fixed pixels")
|
||||
(box :id "size-px" :width (px 180) :height (lh 2)
|
||||
:bgcolor "#D97757" :text-align center "180 px")
|
||||
(box "Fixed width: 180 px."))
|
||||
(column :id "size-percent-parent" :width (px 222)
|
||||
:border "#B85C3C" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||
":width (% 50)\n% / parent content")
|
||||
(box :id "size-percent" :width (% 50) :height (lh 2)
|
||||
:bgcolor "#D97757" :text-align center "110 px")
|
||||
(box "50% of 220 px = 110 px."))
|
||||
(column :id "size-vw-parent" :width (px 222)
|
||||
:border "#B85C3C" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||
":width (vw 20)\n:max-width (% 100)")
|
||||
(box :id "size-vw" :width (vw 20) :max-width (% 100) :height (lh 2)
|
||||
:bgcolor "#D97757" :text-align center "20 vw")
|
||||
(box "20% of viewport width.\nCapped at 220 px."))
|
||||
(column :width (px 222) :border "#B85C3C" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||
":width (ch 12)\n:max-width (% 100)")
|
||||
(box :id "size-ch" :width (ch 12) :max-width (% 100) :height (lh 2)
|
||||
:bgcolor "#D97757" :text-align center "000000000000")
|
||||
(box "Twelve zero advances.\nCapped at 220 px."))
|
||||
(column :width (px 222) :border "#B85C3C" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||
":height (lh 2)\nLH / line count")
|
||||
(box :id "size-lh" :height (lh 2) :bgcolor "#D97757"
|
||||
:text-align center "line one\nline two")
|
||||
(box "Two lines in this font."))
|
||||
(column :width (px 222) :border "#B85C3C" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||
":height (vh 10)\nVH / viewport height")
|
||||
(box :id "size-vh" :height (vh 10) :bgcolor "#D97757"
|
||||
:text-align center "10 vh")
|
||||
(box "10% of viewport lines,\nfloored to whole lines.")))
|
||||
(box :height (lh 1))
|
||||
|
||||
(box :padding-inline (px 6) :border "#5E7F6A" :bgcolor "#DCEBDD"
|
||||
"02 / FONT AND HEIGHT CONTEXTS\nOnly the measured text changes font. Percentage height needs a definite parent.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#5E7F6A" :border-right "#5E7F6A"
|
||||
:border-bottom "#5E7F6A" :bgcolor "#F3FAF4"
|
||||
(column :id "size-font-small-context" :width (px 222)
|
||||
:border "#5E7F6A" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#5E7F6A" :bgcolor "#BFD6C2" :text-align center
|
||||
":font-size 14\n:width (ch 8)\n:height (lh 2)")
|
||||
(box :id "size-font-small" :width (ch 8) :height (lh 2)
|
||||
:font-size 14 :bgcolor "#8FAF95" "00000000\nsmall")
|
||||
(box "Eight zeros, two lines."))
|
||||
(column :id "size-font-large-context" :width (px 222)
|
||||
:border "#5E7F6A" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#5E7F6A" :bgcolor "#BFD6C2" :text-align center
|
||||
":font-size 22\n:width (ch 8)\n:height (lh 2)")
|
||||
(box :id "size-font-large" :width (ch 8) :height (lh 2)
|
||||
:font-size 22 :bgcolor "#8FAF95" "00000000\nlarge")
|
||||
(box "Same units, larger font."))
|
||||
(column :width (px 222) :border "#5E7F6A" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#5E7F6A" :bgcolor "#BFD6C2" :text-align center
|
||||
":height (% 50)\nParent: :height (lh 6)\nResult: three lines")
|
||||
(column :id "size-percent-height-parent" :height (lh 6)
|
||||
(box :id "size-percent-height" :height (% 50) :bgcolor "#8FAF95"
|
||||
:text-align center "50%\nof six lines"))
|
||||
(box "50% of six = three lines.")))
|
||||
(box :height (lh 1))
|
||||
|
||||
;; Short text shows equivalent results; the constrained cases below
|
||||
;; separate intrinsic sizes using the same text and available width.
|
||||
(box :padding-inline (px 6) :border "#3F6D88" :bgcolor "#CAE6F0"
|
||||
"03 / WIDTH KEYWORDS\nStart with red ink in 112 px frames, then constrain longer text to reveal the differences.")
|
||||
(box :padding-inline (px 6) :border-left "#3F6D88" :border-right "#3F6D88"
|
||||
:bgcolor "#F6FBFD" "WIDTH / auto = stretch in a Column. Short text fits, so fit-content = max-content here.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#3F6D88" :border-right "#3F6D88" :border-bottom "#3F6D88" :bgcolor "#F6FBFD"
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":width\nauto")
|
||||
(box :id "size-width-auto" :width auto :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":width\nmin-content")
|
||||
(box :id "size-width-min-content" :width min-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":width\nmax-content")
|
||||
(box :id "size-width-max-content" :width max-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":width\nfit-content")
|
||||
(box :id "size-width-fit-content" :width fit-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":width\nstretch")
|
||||
(box :id "size-width-stretch" :width stretch :height (lh 2) :bgcolor "#7DB8CE" "red ink")))
|
||||
(box :padding-inline (px 6) :border-left "#3F6D88" :border-right "#3F6D88"
|
||||
:bgcolor "#F6FBFD" "MIN-WIDTH / :width (px 24). auto adds no floor; content keywords raise it; stretch reaches the parent.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#3F6D88" :border-right "#3F6D88" :border-bottom "#3F6D88" :bgcolor "#F6FBFD"
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":min-width\nauto")
|
||||
(box :id "size-min-width-auto" :width (px 24) :min-width auto :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":min-width\nmin-content")
|
||||
(box :id "size-min-width-min-content" :width (px 24) :min-width min-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":min-width\nmax-content")
|
||||
(box :id "size-min-width-max-content" :width (px 24) :min-width max-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":min-width\nfit-content")
|
||||
(box :id "size-min-width-fit-content" :width (px 24) :min-width fit-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":min-width\nstretch")
|
||||
(box :id "size-min-width-stretch" :width (px 24) :min-width stretch :height (lh 2) :bgcolor "#7DB8CE" "red ink")))
|
||||
(box :padding-inline (px 6) :border-left "#3F6D88" :border-right "#3F6D88"
|
||||
:bgcolor "#F6FBFD" "MAX-WIDTH / :width (px 112). none adds no cap; stretch also gives 112 px in this parent.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#3F6D88" :border-right "#3F6D88" :border-bottom "#3F6D88" :bgcolor "#F6FBFD"
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":max-width\nnone")
|
||||
(box :id "size-max-width-none" :width (px 112) :max-width none :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":max-width\nmin-content")
|
||||
(box :id "size-max-width-min-content" :width (px 112) :max-width min-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":max-width\nmax-content")
|
||||
(box :id "size-max-width-max-content" :width (px 112) :max-width max-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":max-width\nfit-content")
|
||||
(box :id "size-max-width-fit-content" :width (px 112) :max-width fit-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":max-width\nstretch")
|
||||
(box :id "size-max-width-stretch" :width (px 112) :max-width stretch :height (lh 2) :bgcolor "#7DB8CE" "red ink")))
|
||||
(box :padding-inline (px 6) :border-left "#3F6D88" :border-right "#3F6D88"
|
||||
:bgcolor "#F6FBFD"
|
||||
"LONG CONTENT / Same text, same 8ch parent in each row. Pale ruler = parent width; blue rectangle = actual Box.\nmin-content follows the longest word; fit-content uses 8ch; max-content extends beyond the ruler.\nEach blue rectangle is four lines high. Blank blue space is part of the Box, not extra text.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#3F6D88" :border-right "#3F6D88" :border-bottom "#3F6D88" :bgcolor "#F6FBFD"
|
||||
(column :width (px 222) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center "min-content\nLongest word")
|
||||
(box "width min-content")
|
||||
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||
(column :width (ch 8)
|
||||
(box :id "size-width-long-min-content" :width min-content :height (lh 4)
|
||||
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||
(box "width 1ch + min-width")
|
||||
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||
(column :width (ch 8)
|
||||
(box :id "size-min-width-long-min-content" :width (ch 1) :min-width min-content :height (lh 4)
|
||||
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||
(box "width 20ch + max-width")
|
||||
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||
(column :width (ch 8)
|
||||
(box :id "size-max-width-long-min-content" :width (ch 20) :max-width min-content :height (lh 4)
|
||||
:bgcolor "#7DB8CE" "red ink on blue")))
|
||||
(column :width (px 222) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center "fit-content\nFit available width")
|
||||
(box "width fit-content")
|
||||
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||
(column :width (ch 8)
|
||||
(box :id "size-width-long-fit-content" :width fit-content :height (lh 4)
|
||||
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||
(box "width 1ch + min-width")
|
||||
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||
(column :width (ch 8)
|
||||
(box :id "size-min-width-long-fit-content" :width (ch 1) :min-width fit-content :height (lh 4)
|
||||
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||
(box "width 20ch + max-width")
|
||||
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||
(column :width (ch 8)
|
||||
(box :id "size-max-width-long-fit-content" :width (ch 20) :max-width fit-content :height (lh 4)
|
||||
:bgcolor "#7DB8CE" "red ink on blue")))
|
||||
(column :width (px 222) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center "max-content\nWhole unwrapped text")
|
||||
(box "width max-content")
|
||||
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||
(column :width (ch 8)
|
||||
(box :id "size-width-long-max-content" :width max-content :height (lh 4)
|
||||
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||
(box "width 1ch + min-width")
|
||||
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||
(column :width (ch 8)
|
||||
(box :id "size-min-width-long-max-content" :width (ch 1) :min-width max-content :height (lh 4)
|
||||
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||
(box "width 20ch + max-width")
|
||||
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||
(column :width (ch 8)
|
||||
(box :id "size-max-width-long-max-content" :width (ch 20) :max-width max-content :height (lh 4)
|
||||
:bgcolor "#7DB8CE" "red ink on blue"))))
|
||||
(box :padding-inline (px 6) :border-left "#3F6D88" :border-right "#3F6D88"
|
||||
:bgcolor "#F6FBFD"
|
||||
"ROW CONTEXT / Same 112 px available. auto follows content in a Row; stretch fills the width.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#3F6D88" :border-right "#3F6D88" :border-bottom "#3F6D88" :bgcolor "#F6FBFD"
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center "Row child\n:width auto")
|
||||
(row
|
||||
(box :id "size-width-row-auto" :width auto :height (lh 2) :bgcolor "#7DB8CE" "red ink")))
|
||||
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center "Row child\n:width stretch")
|
||||
(row
|
||||
(box :id "size-width-row-stretch" :width stretch :height (lh 2) :bgcolor "#7DB8CE" "red ink"))))
|
||||
(box :height (lh 1))
|
||||
|
||||
(box :padding-inline (px 6) :border "#7C6A2F" :bgcolor "#EEE1A8"
|
||||
"04 / HEIGHT KEYWORDS\nFive-line measuring surfaces. Ebox gives min-content, max-content, and fit-content the same natural height.\nThese three keywords intentionally match: three content lines. stretch fills all five parent lines.")
|
||||
(box :padding-inline (px 6) :border-left "#7C6A2F" :border-right "#7C6A2F"
|
||||
:bgcolor "#FFFDF0" "HEIGHT / three text lines, with automatic, content, or stretch sizing.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#7C6A2F" :border-right "#7C6A2F" :border-bottom "#7C6A2F" :bgcolor "#FFFDF0"
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":height\nauto")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-height-auto" :height auto :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":height\nmin-content")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-height-min-content" :height min-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":height\nmax-content")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-height-max-content" :height max-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":height\nfit-content")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-height-fit-content" :height fit-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":height\nstretch")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-height-stretch" :height stretch :bgcolor "#D9C46D" "red\nink\nhere"))))
|
||||
(box :padding-inline (px 6) :border-left "#7C6A2F" :border-right "#7C6A2F"
|
||||
:bgcolor "#FFFDF0" "MIN-HEIGHT / :height (lh 1). auto adds no floor; content floors to three; stretch floors to five.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#7C6A2F" :border-right "#7C6A2F" :border-bottom "#7C6A2F" :bgcolor "#FFFDF0"
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":min-height\nauto")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-min-height-auto" :height (lh 1) :min-height auto :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":min-height\nmin-content")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-min-height-min-content" :height (lh 1) :min-height min-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":min-height\nmax-content")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-min-height-max-content" :height (lh 1) :min-height max-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":min-height\nfit-content")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-min-height-fit-content" :height (lh 1) :min-height fit-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":min-height\nstretch")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-min-height-stretch" :height (lh 1) :min-height stretch :bgcolor "#D9C46D" "red\nink\nhere"))))
|
||||
(box :padding-inline (px 6) :border-left "#7C6A2F" :border-right "#7C6A2F"
|
||||
:bgcolor "#FFFDF0" "MAX-HEIGHT / :height (lh 5). Content caps to three; none and stretch both allow five here.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#7C6A2F" :border-right "#7C6A2F" :border-bottom "#7C6A2F" :bgcolor "#FFFDF0"
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":max-height\nnone")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-max-height-none" :height (lh 5) :max-height none :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":max-height\nmin-content")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-max-height-min-content" :height (lh 5) :max-height min-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":max-height\nmax-content")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-max-height-max-content" :height (lh 5) :max-height max-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":max-height\nfit-content")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-max-height-fit-content" :height (lh 5) :max-height fit-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":max-height\nstretch")
|
||||
(column :height (lh 5)
|
||||
(box :id "size-max-height-stretch" :height (lh 5) :max-height stretch :bgcolor "#D9C46D" "red\nink\nhere"))))
|
||||
(box :height (lh 1))
|
||||
|
||||
(box :padding-inline (px 6) :border "#6F6A95" :bgcolor "#E4DEF4"
|
||||
"05 / SIZE FUNCTIONS\nEvery operand follows the property's axis. The white frames have 220 px of content.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#6F6A95" :border-right "#6F6A95"
|
||||
:border-bottom "#6F6A95" :bgcolor "#FBF9FF"
|
||||
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||
":width (calc (- (% 100) (px 24)))")
|
||||
(box :id "size-calc" :width (calc (- (% 100) (px 24))) :height (lh 2)
|
||||
:bgcolor "#AFA3D1" :text-align center "196 px")
|
||||
(box "220 - 24 = 196 px."))
|
||||
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||
":width (min (% 75) (px 100))")
|
||||
(box :id "size-min" :width (min (% 75) (px 100)) :height (lh 2)
|
||||
:bgcolor "#AFA3D1" :text-align center "100 px")
|
||||
(box "min(165, 100) = 100 px."))
|
||||
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||
":width (max (px 60) (% 50))")
|
||||
(box :id "size-max" :width (max (px 60) (% 50)) :height (lh 2)
|
||||
:bgcolor "#AFA3D1" :text-align center "110 px")
|
||||
(box "max(60, 110) = 110 px."))
|
||||
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||
":width (clamp (px 48) (vw 12) (px 200))")
|
||||
(box :id "size-clamp" :width (clamp (px 48) (vw 12) (px 200)) :height (lh 2)
|
||||
:bgcolor "#AFA3D1" :text-align center "clamp")
|
||||
(box "12vw, bounded to 48..200px."))
|
||||
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||
":width (calc (/ (* (+ (px 24) (px 16)) 3) 2))")
|
||||
(box :id "size-arithmetic" :width (calc (/ (* (+ (px 24) (px 16)) 3) 2))
|
||||
:height (lh 2) :bgcolor "#AFA3D1" :text-align center "60 px")
|
||||
(box "(24 + 16) x 3 / 2 = 60 px."))
|
||||
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||
":width (min (% 100) (max (px 80) (vw 20)))")
|
||||
(box :id "size-nested" :width (min (% 100) (max (px 80) (vw 20)))
|
||||
:height (lh 2) :bgcolor "#AFA3D1" :text-align center "nested")
|
||||
(box "20vw, bounded to 80..220px.")))
|
||||
(box :padding-inline (px 6) :border-left "#6F6A95" :border-right "#6F6A95"
|
||||
:bgcolor "#FBF9FF"
|
||||
"COMPARE FUNCTIONS / Same input X in each card; fixed bounds 48..144 px.\nmin caps the top; max floors the bottom; clamp does both. Compare all three input ranges at once.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#6F6A95" :border-right "#6F6A95" :border-bottom "#6F6A95" :bgcolor "#FBF9FF"
|
||||
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center "LOW / X = 24 px\nBelow the 48 px floor")
|
||||
(box "min(X, 144) = 24 px")
|
||||
(box :id "size-function-min-low" :width (min (px 24) (px 144)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||
(box "max(48, X) = 48 px")
|
||||
(box :id "size-function-max-low" :width (max (px 48) (px 24)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||
(box "clamp(48, X, 144) = 48 px")
|
||||
(box :id "size-function-clamp-low" :width (clamp (px 48) (px 24) (px 144)) :height (lh 1) :bgcolor "#AFA3D1"))
|
||||
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center "MID / X = 96 px\nInside the 48..144 px range")
|
||||
(box "min(X, 144) = 96 px")
|
||||
(box :id "size-function-min-mid" :width (min (px 96) (px 144)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||
(box "max(48, X) = 96 px")
|
||||
(box :id "size-function-max-mid" :width (max (px 48) (px 96)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||
(box "clamp(48, X, 144) = 96 px")
|
||||
(box :id "size-function-clamp-mid" :width (clamp (px 48) (px 96) (px 144)) :height (lh 1) :bgcolor "#AFA3D1"))
|
||||
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center "HIGH / X = 192 px\nAbove the 144 px cap")
|
||||
(box "min(X, 144) = 144 px")
|
||||
(box :id "size-function-min-high" :width (min (px 192) (px 144)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||
(box "max(48, X) = 192 px")
|
||||
(box :id "size-function-max-high" :width (max (px 48) (px 192)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||
(box "clamp(48, X, 144) = 144 px")
|
||||
(box :id "size-function-clamp-high" :width (clamp (px 48) (px 192) (px 144)) :height (lh 1) :bgcolor "#AFA3D1")))
|
||||
(box :height (lh 1))
|
||||
|
||||
(box :padding-inline (px 6) :border "#3D7068" :bgcolor "#CDEBE5"
|
||||
"06 / BOX MODEL, FLEX, AND GRID\nThe same axis rules size edges, gaps, bases, and tracks.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#3D7068" :border-right "#3D7068"
|
||||
:border-bottom "#3D7068" :bgcolor "#F7FCFB"
|
||||
(column :width (px 222) :border "#3D7068" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3D7068" :bgcolor "#CDEBE5" :text-align center
|
||||
":box-sizing border-box\n:padding ((lh 1) (ch 1))")
|
||||
(box :id "size-box-model" :width (px 180) :height (lh 4) :box-sizing border-box
|
||||
:padding ((lh 1) (ch 1)) :margin ((lh 0) (px 6))
|
||||
:border ((px 1) solid "#3D7068") :bgcolor "#9FD5CB"
|
||||
"padding\ninside the border")
|
||||
(box "180px outer; 6px margins.\n1lh/1ch padding; 1px stroke."))
|
||||
(column :width (px 222) :border "#3D7068" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3D7068" :bgcolor "#CDEBE5" :text-align center
|
||||
":flex (1 1 (ch 12))\n:flex (2 1 (px 96))")
|
||||
(flex :id "size-flex" :flex-wrap wrap :gap ((lh 1) (px 10))
|
||||
(box :id "size-flex-basis" :flex (1 1 (ch 12)) :min-width (px 0)
|
||||
:height (lh 2) :bgcolor "#9FD5CB" :text-align center "1 share\n12ch")
|
||||
(box :flex (2 1 (px 96)) :min-width (px 0) :height (lh 2)
|
||||
:bgcolor "#71BDB0" :text-align center "2 shares\n96px"))
|
||||
(box "Bases: 12ch and 96px.\nLarger fonts may wrap items.\nGap: 1lh x 10px."))
|
||||
(column :width (px 222) :border "#3D7068" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3D7068" :bgcolor "#CDEBE5" :text-align center
|
||||
":flex (1 1 (px 30))\n:flex (2 1 (px 30))")
|
||||
(flex :id "size-flex-grow" :column-gap (px 10)
|
||||
(box :id "size-flex-grow-one" :flex (1 1 (px 30)) :min-width (px 0)
|
||||
:height (lh 2) :bgcolor "#9FD5CB" :text-align center "80px\n+50px")
|
||||
(box :id "size-flex-grow-two" :flex (2 1 (px 30)) :min-width (px 0)
|
||||
:height (lh 2) :bgcolor "#71BDB0" :text-align center "130px\n+100px"))
|
||||
(box "Same 30px base; 10px gap.\n150px extra splits 1:2.\nTotal widths: 80px / 130px."))
|
||||
(column :width (px 222) :border "#3D7068" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#3D7068" :bgcolor "#CDEBE5" :text-align center
|
||||
":grid-template-columns ((ch 8) (fr 1))\n:grid-template-rows ((lh 2) (lh 2))")
|
||||
(grid :id "size-grid" :grid-template-columns ((ch 8) (fr 1))
|
||||
:grid-template-rows ((lh 2) (lh 2)) :gap ((lh 1) (ch 1))
|
||||
(box :bgcolor "#9FD5CB" :text-align center "8ch")
|
||||
(box :bgcolor "#71BDB0" :text-align center "1fr")
|
||||
(box :bgcolor "#9FD5CB" :text-align center "2lh")
|
||||
(box :bgcolor "#71BDB0" :text-align center "2lh"))
|
||||
(box "Two 2lh rows.\nGap: 1lh x 1ch.")))
|
||||
(box :height (lh 1))
|
||||
|
||||
(box :padding-inline (px 6) :border "#9A6B43" :bgcolor "#F1D0AF"
|
||||
"07 / PRECISION AND BOUNDARIES\nHorizontal calculations retain fractions. Final vertical sizes use whole lines.\n2.75lh and 2lh intentionally render alike: both resolve to two whole lines.")
|
||||
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||
:border-left "#9A6B43" :border-right "#9A6B43"
|
||||
:border-bottom "#9A6B43" :bgcolor "#FFFAF5"
|
||||
(column :width (px 222) :border "#9A6B43" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#9A6B43" :bgcolor "#F1D0AF" :text-align center
|
||||
":width (vw 12)\n:max-width (% 100)")
|
||||
(box :id "size-fractional-width" :width (vw 12) :max-width (% 100)
|
||||
:height (lh 2) :bgcolor "#D9AD83" :text-align center "12 vw")
|
||||
(box "853px viewport: 102.36px.\nCapped at 220px content."))
|
||||
(column :width (px 222) :border "#9A6B43" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#9A6B43" :bgcolor "#F1D0AF" :text-align center
|
||||
":height (lh 2.75)\n:overflow hidden")
|
||||
(box :id "size-fractional-height" :height (lh 2.75) :overflow hidden
|
||||
:bgcolor "#D9AD83" :text-align center "line one\nline two\nline three")
|
||||
(box "Floors to two visible lines.\nThe third line is clipped."))
|
||||
(column :width (px 222) :border "#9A6B43" :bgcolor "#FFFDF8"
|
||||
(box :border-bottom "#9A6B43" :bgcolor "#F1D0AF" :text-align center
|
||||
":height (lh 2)\n:overflow hidden")
|
||||
(box :id "size-whole-height" :height (lh 2) :overflow hidden
|
||||
:bgcolor "#D9AD83" :text-align center "line one\nline two\nline three")
|
||||
(box "Exactly two visible lines.\nThe third line is clipped.")))
|
||||
(box :padding-inline (px 6) :border-left "#9A6B43" :border-right "#9A6B43"
|
||||
:border-bottom "#9A6B43" :bgcolor "#FFFAF5"
|
||||
"Empty auto-sized boxes occupy zero lines; use :height (lh 1) for a spacer.\nVertical properties reject px/ch/vw, including function operands. Border strokes reject percentages."))
|
||||
@ -13,14 +13,21 @@
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'ebox-playground)
|
||||
(require 'ebox-native-reflow)
|
||||
|
||||
(defconst ebox-playground-flex-resize-evaluator--fixture
|
||||
(expand-file-name "examples/flex-reference.ebox"
|
||||
ebox-playground-directory)
|
||||
"Flex fixture used by the performance evaluator.")
|
||||
(defcustom ebox-playground-flex-resize-evaluator-fixture
|
||||
(or (getenv "EBOX_PLAYGROUND_FLEX_FIXTURE")
|
||||
(expand-file-name "examples/flex-reference.ebox"
|
||||
ebox-playground-directory))
|
||||
"Read-only Flex fixture used by the performance evaluator."
|
||||
:type 'file
|
||||
:group 'ebox-playground)
|
||||
|
||||
(defcustom ebox-playground-flex-resize-evaluator-max-seconds 0.5
|
||||
"Maximum allowed end-to-end time for one settled preview resize."
|
||||
(defconst ebox-playground-flex-resize-evaluator-warmup-count 5)
|
||||
(defconst ebox-playground-flex-resize-evaluator-sample-count 30)
|
||||
|
||||
(defcustom ebox-playground-flex-resize-evaluator-max-seconds 2.0
|
||||
"Maximum allowed end-to-end time for the measured resize burst."
|
||||
:type 'number
|
||||
:group 'ebox-playground)
|
||||
|
||||
@ -37,38 +44,198 @@
|
||||
(defvar ebox-playground-flex-resize-evaluator--output-lines nil
|
||||
"Report lines collected during one evaluator run.")
|
||||
|
||||
(defun ebox-playground-flex-resize-evaluator--percentile
|
||||
(samples percentile)
|
||||
"Return nearest-rank PERCENTILE from numeric SAMPLES."
|
||||
(unless samples
|
||||
(error "Flex evaluator cannot summarize empty samples"))
|
||||
(let* ((ordered (sort (copy-sequence samples) #'<))
|
||||
(rank (max 1 (ceiling (* percentile (length ordered))))))
|
||||
(nth (1- rank) ordered)))
|
||||
|
||||
(defconst ebox-playground-flex-resize-evaluator--runtime-properties
|
||||
(delete-dups
|
||||
(append (mapcar #'cdr ebox-region-types)
|
||||
'(ebox-content-idx ebox-content-owners ebox-scroll-window)))
|
||||
'(ebox-content-idx ebox-content-owners ebox-scroll-window
|
||||
ebox--paint-origin)))
|
||||
"Ebox identity properties omitted from fresh-render comparison.")
|
||||
|
||||
(defun ebox-playground-flex-resize-evaluator--preview-buffer ()
|
||||
"Return the preview buffer for the fixed flex fixture."
|
||||
(get-buffer
|
||||
(format "*Ebox Preview: %s*"
|
||||
ebox-playground-flex-resize-evaluator--fixture)))
|
||||
|
||||
(defun ebox-playground-flex-resize-evaluator--line-widths (buffer)
|
||||
"Return pixel widths for every rendered line in BUFFER."
|
||||
(with-current-buffer buffer
|
||||
(mapcar #'ebox-string-pixel-width
|
||||
(ebox-string-lines (buffer-string)))))
|
||||
|
||||
(defun ebox-playground-flex-resize-evaluator--merge-face (higher lower)
|
||||
"Merge HIGHER priority face attributes into LOWER without losing relative sizes."
|
||||
(let ((result (copy-sequence lower)))
|
||||
(while higher
|
||||
(let ((attribute (pop higher))
|
||||
(value (pop higher)))
|
||||
(unless (eq value 'unspecified)
|
||||
(setq result
|
||||
(plist-put result attribute
|
||||
(if (plist-member result attribute)
|
||||
(merge-face-attribute
|
||||
attribute value (plist-get result attribute))
|
||||
value))))))
|
||||
result))
|
||||
|
||||
(defun ebox-playground-flex-resize-evaluator--effective-face (face &optional seen)
|
||||
"Return sorted effective attributes of FACE, guarding inheritance with SEEN.
|
||||
Face lists put higher priority entries first. Relative heights are merged using
|
||||
Emacs's own attribute operation; named faces use the current display frame."
|
||||
(let
|
||||
((attributes
|
||||
(cond
|
||||
((null face) nil)
|
||||
((facep face)
|
||||
(when (member face seen)
|
||||
(error "Cyclic face inheritance: %S" face))
|
||||
(ebox-playground-flex-resize-evaluator--effective-face
|
||||
(cl-mapcan (lambda (entry) (list (car entry) (cdr entry)))
|
||||
(face-all-attributes face (selected-frame)))
|
||||
(cons face seen)))
|
||||
((and (proper-list-p face) (keywordp (car face)))
|
||||
(unless (zerop (% (length face) 2))
|
||||
(error "Invalid anonymous face: %S" face))
|
||||
(let ((own (copy-sequence face))
|
||||
(inherited (plist-get face :inherit)))
|
||||
(cl-remf own :inherit)
|
||||
(ebox-playground-flex-resize-evaluator--merge-face
|
||||
own
|
||||
(unless (memq inherited '(nil unspecified))
|
||||
(ebox-playground-flex-resize-evaluator--effective-face
|
||||
inherited seen)))))
|
||||
((proper-list-p face)
|
||||
(let (result)
|
||||
(dolist (entry (reverse face))
|
||||
(setq result
|
||||
(ebox-playground-flex-resize-evaluator--merge-face
|
||||
(ebox-playground-flex-resize-evaluator--effective-face
|
||||
entry seen)
|
||||
result)))
|
||||
result))
|
||||
(t (error "Unsupported face value in render comparison: %S" face)))))
|
||||
(cl-loop for key in (sort (cl-loop for (key _value) on attributes by #'cddr
|
||||
collect key)
|
||||
(lambda (a b)
|
||||
(string-lessp (symbol-name a) (symbol-name b))))
|
||||
append (list key (plist-get attributes key)))))
|
||||
|
||||
(defun ebox-playground-flex-resize-evaluator--visual-text (text)
|
||||
"Return TEXT with fresh-render identity properties removed.
|
||||
Fresh renders allocate different region ids by design. Keep character,
|
||||
face, display, and all non-runtime properties so the comparison still checks
|
||||
the visible right-hand surface rather than merely checking that rendering
|
||||
completed."
|
||||
(let ((copy (copy-sequence text)))
|
||||
"Return TEXT with runtime ids removed and face cascades normalized.
|
||||
Keep characters, display dimensions, effective face attributes, and interactive
|
||||
properties. Equivalent anonymous face stacks compare equal; changes to visible
|
||||
color, relative font size, measurement, or interaction still compare unequal."
|
||||
(let ((copy (copy-sequence text))
|
||||
(hover-owners (make-hash-table :test #'eql)))
|
||||
(when (> (length copy) 0)
|
||||
(remove-list-of-text-properties
|
||||
0 (length copy)
|
||||
ebox-playground-flex-resize-evaluator--runtime-properties
|
||||
copy))
|
||||
copy)
|
||||
;; Fresh renders allocate different owner ids. Preserve the owner
|
||||
;; partition, while comparing all of the retained hover paint facts.
|
||||
(let ((start 0) end)
|
||||
(while (< start (length copy))
|
||||
(setq end (next-single-property-change
|
||||
start 'ebox--hover-style copy (length copy)))
|
||||
(when-let* ((declaration (get-text-property start 'ebox--hover-style copy))
|
||||
(owner (plist-get declaration :owner)))
|
||||
(put-text-property
|
||||
start end 'ebox--hover-style
|
||||
(plist-put
|
||||
(copy-sequence declaration) :owner
|
||||
(or (gethash owner hover-owners)
|
||||
(puthash owner (1+ (hash-table-count hover-owners)) hover-owners)))
|
||||
copy))
|
||||
(setq start end)))
|
||||
(dolist (property '(face font-lock-face mouse-face))
|
||||
(let ((start 0) end)
|
||||
(while (< start (length copy))
|
||||
(setq end (next-single-property-change
|
||||
start property copy (length copy)))
|
||||
(when-let* ((face (get-text-property start property copy)))
|
||||
(put-text-property
|
||||
start end property
|
||||
(ebox-playground-flex-resize-evaluator--effective-face face)
|
||||
copy))
|
||||
(setq start end)))))
|
||||
copy))
|
||||
|
||||
(defun ebox-playground-flex-resize-evaluator--example-text (file width full)
|
||||
"Render FILE at WIDTH, using an unclipped document copy when FULL is non-nil."
|
||||
(let ((ebox-viewport-width width)
|
||||
(ebox-viewport-height 36))
|
||||
(if full
|
||||
(let ((form (ebox-playground--evaluate-form
|
||||
(ebox-playground--read-file file) file)))
|
||||
;; These changes affect only the comparison copy, never the author file.
|
||||
(cl-remf (cdr form) :height)
|
||||
(cl-remf (cdr form) :overflow)
|
||||
(setcdr form (append '(:height auto :overflow visible) (cdr form)))
|
||||
(ebox-render (ebox-build form)))
|
||||
(ebox-render (ebox-playground-view file width)))))
|
||||
|
||||
(defun ebox-playground-compare-example-directories
|
||||
(baseline-directory current-directory &optional widths files)
|
||||
"Compare FILES in BASELINE-DIRECTORY and CURRENT-DIRECTORY at WIDTHS.
|
||||
Default FILES are the Flex and Grid references; default WIDTHS are 720 and 1000.
|
||||
Compare both the authored 36-line viewport and the complete document so changes
|
||||
below the fold cannot pass unnoticed. Print bounded results and signal an error
|
||||
on changed text, effective face, display, interaction, or measured line width.
|
||||
No files, windows, buffers, or persistent render state are modified."
|
||||
(let (results failures)
|
||||
(dolist (file (or files '("flex-reference.ebox" "grid-reference.ebox")))
|
||||
(dolist (width (or widths '(720 1000)))
|
||||
(unless (and (numberp width) (> width 0))
|
||||
(error "Comparison viewport must be a positive number: %S" width))
|
||||
(dolist (full '(nil t))
|
||||
(let* ((before (ebox-playground-flex-resize-evaluator--example-text
|
||||
(expand-file-name file baseline-directory) width full))
|
||||
(after (ebox-playground-flex-resize-evaluator--example-text
|
||||
(expand-file-name file current-directory) width full))
|
||||
(before-widths (mapcar #'ebox-string-pixel-width
|
||||
(ebox-string-lines before)))
|
||||
(after-widths (mapcar #'ebox-string-pixel-width
|
||||
(ebox-string-lines after)))
|
||||
(left (ebox-playground-flex-resize-evaluator--visual-text before))
|
||||
(right (ebox-playground-flex-resize-evaluator--visual-text after))
|
||||
(same-widths (and (= (length before-widths) (length after-widths))
|
||||
(cl-every #'= before-widths after-widths)))
|
||||
(same-text (equal-including-properties left right))
|
||||
(same (and same-widths same-text))
|
||||
(result (list :file file :width width
|
||||
:mode (if full 'document 'viewport)
|
||||
:equal same :line-widths-equal same-widths
|
||||
:before-length (length before)
|
||||
:after-length (length after))))
|
||||
(princ (format "%s %s width=%s mode=%s characters=%s/%s line-widths=%s\n"
|
||||
(if same "PASS" "FAIL") file width
|
||||
(if full 'document 'viewport)
|
||||
(length before) (length after) same-widths))
|
||||
(unless same
|
||||
(let* ((position
|
||||
(cl-loop for index below (min (length left) (length right))
|
||||
unless (equal-including-properties
|
||||
(substring left index (1+ index))
|
||||
(substring right index (1+ index)))
|
||||
return index))
|
||||
(print-level 4) (print-length 12))
|
||||
(when position
|
||||
(princ (format " first difference at character %s: %S -> %S\n"
|
||||
position
|
||||
(list (aref left position)
|
||||
(text-properties-at position left))
|
||||
(list (aref right position)
|
||||
(text-properties-at position right))))))
|
||||
(push result failures))
|
||||
(push result results)))))
|
||||
(when failures
|
||||
(error "Example render comparison failed in %d cases" (length failures)))
|
||||
(nreverse results)))
|
||||
|
||||
(defun ebox-playground-flex-resize-evaluator--check (name condition detail)
|
||||
"Record a named check and return whether CONDITION passed."
|
||||
(push (format "%s %s -- %s"
|
||||
@ -76,6 +243,366 @@ completed."
|
||||
ebox-playground-flex-resize-evaluator--output-lines)
|
||||
condition)
|
||||
|
||||
(defun ebox-playground-scroll-evaluator--measure (region-id steps)
|
||||
"Measure STEPS alternating cached one-line scrolls of REGION-ID."
|
||||
(let ((root-renders 0) (node-styles 0) (style-computations 0)
|
||||
samples installed)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(dolist (entry
|
||||
(list
|
||||
(cons 'ebox-surface--render-candidate
|
||||
(lambda (&rest _) (cl-incf root-renders)))
|
||||
(cons 'ebox-surface--apply-node-style
|
||||
(lambda (&rest _) (cl-incf node-styles)))
|
||||
(cons 'ebox-surface--compute-node-style
|
||||
(lambda (&rest _) (cl-incf style-computations)))))
|
||||
(advice-add (car entry) :before (cdr entry))
|
||||
(push entry installed))
|
||||
(dotimes (index steps)
|
||||
(let* ((delta (if (zerop (% index 2)) 1 -1))
|
||||
(start (float-time))
|
||||
(consumed (ebox--scroll-region-by region-id delta 1)))
|
||||
(push (* 1000.0 (- (float-time) start)) samples)
|
||||
(unless (equal consumed delta)
|
||||
(error "Scroll evaluator moved %S lines, expected %S"
|
||||
consumed delta))))
|
||||
(list :mean-ms (/ (apply #'+ samples) (float steps))
|
||||
:p95-ms (ebox-playground-flex-resize-evaluator--percentile
|
||||
samples 0.95)
|
||||
:max-ms (apply #'max samples)
|
||||
:root-renders root-renders :node-styles node-styles
|
||||
:style-computations style-computations))
|
||||
(dolist (entry installed)
|
||||
(advice-remove (car entry) (cdr entry))))))
|
||||
|
||||
(defun ebox-playground-scroll-evaluator--fresh-parity (buffer file)
|
||||
"Compare BUFFER with an independent FILE render at its root scroll offset."
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(ebox-viewport-width (plist-get state :viewport-width))
|
||||
(ebox-viewport-height (plist-get state :viewport-height))
|
||||
(region-id (plist-get (plist-get state :root-node) :region-id))
|
||||
(offset (plist-get (gethash region-id (plist-get state :scroll-state-table))
|
||||
:scroll-offset))
|
||||
offset-set
|
||||
(set-offset
|
||||
(lambda (root)
|
||||
(unless offset-set
|
||||
(setq offset-set t)
|
||||
(ebox-put root :scroll-offset (or offset 0))
|
||||
(ebox-put root :ebox-scroll-offset-controlled-p t))))
|
||||
expected)
|
||||
;; Scroll offset is runtime state, not an authorable DSL style property.
|
||||
;; Set it on the independent candidate immediately before its first render.
|
||||
(unwind-protect
|
||||
(progn
|
||||
(advice-add 'ebox--render-layout :before set-offset)
|
||||
(setq expected (ebox-render (ebox-playground-view
|
||||
file ebox-viewport-width)))
|
||||
(unless offset-set
|
||||
(error "Fresh scroll oracle did not reach the root layout")))
|
||||
(advice-remove 'ebox--render-layout set-offset))
|
||||
(with-current-buffer buffer
|
||||
(let* ((actual (ebox-playground-scroll-evaluator--visual-text
|
||||
(buffer-string)))
|
||||
(fresh (ebox-playground-scroll-evaluator--visual-text expected))
|
||||
(same (equal-including-properties actual fresh)))
|
||||
(unless same
|
||||
(let ((position
|
||||
(cl-loop for index below (min (length actual) (length fresh))
|
||||
unless (equal-including-properties
|
||||
(substring actual index (1+ index))
|
||||
(substring fresh index (1+ index)))
|
||||
return index))
|
||||
(print-level 4) (print-length 12))
|
||||
(princ (format " render lengths=%s/%s first-difference=%S %S -> %S\n"
|
||||
(length actual) (length fresh) position
|
||||
(and position (substring actual position
|
||||
(1+ position)))
|
||||
(and position (substring fresh position
|
||||
(1+ position)))))))
|
||||
same))))
|
||||
|
||||
(defun ebox-playground-scroll-evaluator--visual-text (text)
|
||||
"Normalize visible TEXT, ignoring identity and inert zero-pixel spacers.
|
||||
Fresh layout may retain empty padding spaces that materialized scroll content
|
||||
omits. Drop one only beside a visible character with identical faces, so line
|
||||
font metrics are preserved. Retain height, ascent and alignment carriers."
|
||||
(let ((text (ebox-playground-flex-resize-evaluator--visual-text text))
|
||||
(start 0) parts)
|
||||
(dotimes (index (length text))
|
||||
(when (and (= (aref text index) ?\s)
|
||||
(equal (get-text-property index 'display text)
|
||||
'(space :width (0)))
|
||||
(cl-some
|
||||
(lambda (neighbor)
|
||||
(and (<= 0 neighbor) (< neighbor (length text))
|
||||
(/= (aref text neighbor) ?\n)
|
||||
(let ((display (get-text-property neighbor 'display text)))
|
||||
(or (null display)
|
||||
(pcase display
|
||||
(`(space :width (,width))
|
||||
(and (numberp width) (> width 0))))))
|
||||
(cl-every
|
||||
(lambda (property)
|
||||
(equal (get-text-property index property text)
|
||||
(get-text-property neighbor property text)))
|
||||
'(face font-lock-face))))
|
||||
(list (1- index) (1+ index))))
|
||||
(push (substring text start index) parts)
|
||||
(setq start (1+ index))))
|
||||
(push (substring text start) parts)
|
||||
(apply #'concat (nreverse parts))))
|
||||
|
||||
(defun ebox-playground-interaction-evaluator-click (buffer label &optional offset)
|
||||
"Simulate a native mouse-1 at visible LABEL in BUFFER, plus character OFFSET.
|
||||
This GUI diagnostic resolves the actual text-property keymap with an Emacs
|
||||
mouse event and invokes its interactive command. It does not call a business
|
||||
action directly, move point, select a window or generate an OS mouse event.
|
||||
Return the clicked position; the caller must verify the application's result."
|
||||
(setq buffer (get-buffer buffer) offset (or offset 0))
|
||||
(unless (and (buffer-live-p buffer) (stringp label) (> (length label) 0)
|
||||
(integerp offset) (<= 0 offset) (< offset (length label)))
|
||||
(error "Click diagnostic needs a live buffer, label and in-label offset"))
|
||||
(let* ((window (or (get-buffer-window buffer t)
|
||||
(error "Click diagnostic target is not visible")))
|
||||
(position
|
||||
(with-current-buffer buffer
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(unless (search-forward label nil t)
|
||||
(error "Click diagnostic label is missing: %s" label))
|
||||
(+ (- (point) (length label)) offset))))
|
||||
(posn (or (posn-at-point position window)
|
||||
(error "Click diagnostic label is outside the visible window")))
|
||||
(event (list 'mouse-1 posn))
|
||||
(command (key-binding (vector event)))
|
||||
(native (with-current-buffer buffer
|
||||
(get-text-property position 'keymap))))
|
||||
(unless (and (keymapp native) (commandp command)
|
||||
(eq command (lookup-key native [mouse-1])))
|
||||
(error "Click diagnostic label has no native mouse-1 command"))
|
||||
(let ((last-input-event event)) (call-interactively command))
|
||||
position))
|
||||
|
||||
(defun ebox-playground-interaction-evaluator-run
|
||||
(file label &optional key steps width height)
|
||||
"Measure repeated native KEY activation at LABEL in FILE.
|
||||
KEY defaults to RET, STEPS to 30, WIDTH to 720 pixels and HEIGHT to 200 lines.
|
||||
Place point at LABEL once, then perform normal key lookup for every command;
|
||||
fail if an update loses that binding. Return command timing in milliseconds,
|
||||
GC counts and time, allocated cons cells, publication count and work counts,
|
||||
and total characters scanned for fragment extraction (including local slices),
|
||||
final plain text and fresh-render parity.
|
||||
Rendering a committed snapshot independently must reproduce the mounted text.
|
||||
|
||||
This measures synchronous commands and publication, including GC, without
|
||||
redisplay, OS key repeat or idle prewarming. It uses current font metrics and
|
||||
a temporary preview, preserving existing buffers, windows and runtime state.
|
||||
Cleanup runs on success, error and quit; stop between samples after 30 seconds."
|
||||
(setq key (or key "RET") steps (or steps 30)
|
||||
width (or width 720) height (or height 200))
|
||||
(unless (and (stringp file) (stringp label) (> (length label) 0)
|
||||
(or (stringp key) (vectorp key)))
|
||||
(error "Interaction evaluator needs a file, nonempty label and native key"))
|
||||
(dolist (value (list steps width height))
|
||||
(unless (and (integerp value) (> value 0))
|
||||
(error "Interaction evaluator dimensions and steps must be positive integers")))
|
||||
(let ((keys (if (stringp key) (kbd key) key))
|
||||
(buffer (generate-new-buffer " *ebox-interaction-evaluator*"))
|
||||
(ebox-viewport-width width) (ebox-viewport-height height)
|
||||
(ebox-runtime-idle-prewarm nil)
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil)
|
||||
(ebox-scroll-lazy-idle-prefetch-lines 0)
|
||||
(gc-cons-threshold gc-cons-threshold)
|
||||
(gc-cons-percentage gc-cons-percentage)
|
||||
(ebox--deferred-render-gc-state nil)
|
||||
(ebox--deferred-render-gc-timer nil)
|
||||
(ebox--deferred-render-gc-depth 0)
|
||||
(ebox--deferred-render-gc-generation 0)
|
||||
(work (list :layout-calls 0 :surface-plans 0 :fragment-scans 0
|
||||
:fragment-characters 0
|
||||
:node-registrations 0))
|
||||
samples installed)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(when (zerop (length keys))
|
||||
(error "Interaction evaluator key must not be empty"))
|
||||
(ebox-playground-open-file file (buffer-name buffer))
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-min))
|
||||
(unless (search-forward label nil t)
|
||||
(error "Interaction label is not visible: %s" label))
|
||||
(backward-char (length label))
|
||||
(let* ((command (key-binding keys))
|
||||
(native (get-text-property (point) 'keymap))
|
||||
(revision (plist-get (ebox-surface-buffer-snapshot buffer)
|
||||
:revision))
|
||||
(gc-count gcs-done) (gc-time gc-elapsed)
|
||||
(cons-count (car (memory-use-counts)))
|
||||
(deadline (+ (float-time) 30)))
|
||||
(unless (and (keymapp native) (commandp command)
|
||||
(eq command (lookup-key native keys)))
|
||||
(error "Interaction label has no native command for %s"
|
||||
(key-description keys)))
|
||||
(dolist (entry '((ebox--render-layout . :layout-calls)
|
||||
(ebox-surface--surface-plan . :surface-plans)
|
||||
(ebox-surface--ensure-node-tree . :node-registrations)))
|
||||
(let* ((key (cdr entry))
|
||||
(advice (lambda (&rest _)
|
||||
(plist-put work key (1+ (plist-get work key))))))
|
||||
(advice-add (car entry) :before advice)
|
||||
(push (cons (car entry) advice) installed)))
|
||||
(let ((advice
|
||||
(lambda (text)
|
||||
(cl-incf (plist-get work :fragment-scans))
|
||||
(cl-incf (plist-get work :fragment-characters)
|
||||
(length text)))))
|
||||
(advice-add 'ebox-surface--rendered-fragments :before advice)
|
||||
(push (cons 'ebox-surface--rendered-fragments advice) installed))
|
||||
(dotimes (_ steps)
|
||||
(when (> (float-time) deadline)
|
||||
(error "Interaction evaluator exceeded 30 seconds"))
|
||||
(unless (eq (key-binding keys) command)
|
||||
(error "Repeated %s lost its node binding at point %d"
|
||||
(key-description keys) (point)))
|
||||
(let ((started (float-time))
|
||||
(last-input-event (aref (vconcat keys) (1- (length keys)))))
|
||||
(call-interactively command)
|
||||
(push (* 1000 (- (float-time) started)) samples)))
|
||||
(dolist (entry installed) (advice-remove (car entry) (cdr entry)))
|
||||
(setq installed nil)
|
||||
(let* ((cons-count (- (car (memory-use-counts)) cons-count))
|
||||
(gc-count (- gcs-done gc-count))
|
||||
(gc-ms (* 1000 (- gc-elapsed gc-time)))
|
||||
(snapshot (ebox-surface-buffer-snapshot buffer))
|
||||
(fresh (ebox-render (plist-get snapshot :input)))
|
||||
(actual (buffer-string))
|
||||
(parity (equal-including-properties
|
||||
(ebox-playground-scroll-evaluator--visual-text actual)
|
||||
(ebox-playground-scroll-evaluator--visual-text fresh))))
|
||||
(unless parity
|
||||
(error "Repeated interaction differs from committed snapshot render"))
|
||||
(append
|
||||
(list :file (expand-file-name file) :label label
|
||||
:key (key-description keys) :steps steps
|
||||
:mean-ms (/ (apply #'+ samples) (float steps))
|
||||
:p95-ms (ebox-playground-flex-resize-evaluator--percentile
|
||||
samples 0.95)
|
||||
:max-ms (apply #'max samples)
|
||||
:gc-count gc-count :gc-ms gc-ms
|
||||
:cons-cells cons-count
|
||||
:publications (- (plist-get snapshot :revision) (or revision 0))
|
||||
:fresh-parity parity :text (substring-no-properties actual))
|
||||
work)))))
|
||||
(dolist (entry installed) (advice-remove (car entry) (cdr entry)))
|
||||
(when (buffer-live-p buffer) (ebox-playground-close (buffer-name buffer)))
|
||||
(when (timerp ebox--deferred-render-gc-timer)
|
||||
(cancel-timer ebox--deferred-render-gc-timer)))))
|
||||
|
||||
(defun ebox-playground-scroll-evaluator-run
|
||||
(files &optional width steps height)
|
||||
"Profile cached root scrolling for FILES at WIDTH with STEPS and HEIGHT.
|
||||
FILES is one `.ebox' path or a list of paths. WIDTH defaults to 720 pixels,
|
||||
STEPS to 20 alternating one-line motions, and HEIGHT to 36 viewport lines.
|
||||
Each file must have a scrollable root. Fully materialize its content outside
|
||||
the samples, then measure cached top and midpoint motion. Return bounded
|
||||
plists with mean, p95 and maximum milliseconds, full root render counts, node
|
||||
style visits, style computations, and fresh-render parity for each position.
|
||||
Signal an error on missing scroll capacity, incomplete motion or render drift.
|
||||
|
||||
Use the current Emacs frame's font metrics without displaying temporary
|
||||
buffers or changing windows. This measures retained TP publication; native
|
||||
window scrolling, idle prewarming and redisplay are excluded. Existing live
|
||||
buffers, timers and runtime tables are preserved. Temporary mounts and advice
|
||||
are cleaned on success, error or quit; no global runtime reset is performed."
|
||||
(setq files (if (stringp files) (list files) files)
|
||||
width (or width 720) steps (or steps 20) height (or height 36))
|
||||
(unless (and files (proper-list-p files) (cl-every #'stringp files))
|
||||
(error "Scroll evaluator needs a file path or nonempty list of paths"))
|
||||
(dolist (value (list width steps height))
|
||||
(unless (and (integerp value) (> value 0))
|
||||
(error "Scroll evaluator dimensions and steps must be positive integers")))
|
||||
(let ((ebox-viewport-width width)
|
||||
(ebox-viewport-height height)
|
||||
(ebox-native-buffer-scroll nil)
|
||||
(ebox-runtime-idle-prewarm nil)
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil)
|
||||
(ebox-scroll-lazy-idle-prefetch-lines 0)
|
||||
;; Isolate the GUI deferred-GC lease, including any timer it creates.
|
||||
(gc-cons-threshold gc-cons-threshold)
|
||||
(gc-cons-percentage gc-cons-percentage)
|
||||
(ebox--deferred-render-gc-state nil)
|
||||
(ebox--deferred-render-gc-timer nil)
|
||||
(ebox--deferred-render-gc-depth 0)
|
||||
(ebox--deferred-render-gc-generation 0)
|
||||
results)
|
||||
(unwind-protect
|
||||
(dolist (file files)
|
||||
(let ((buffer (generate-new-buffer " *ebox-scroll-evaluator*")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-playground-open-file file (buffer-name buffer))
|
||||
(with-current-buffer buffer
|
||||
(let* ((runtime (ebox--buffer-render-state buffer))
|
||||
(region-id (plist-get (plist-get runtime :root-node)
|
||||
:region-id))
|
||||
(table (plist-get runtime :scroll-state-table))
|
||||
(state (and region-id (gethash region-id table)))
|
||||
max-offset)
|
||||
(unless state
|
||||
(error "Scroll evaluator requires a scrollable root: %s"
|
||||
file))
|
||||
(setq state (ebox--scroll-state-materialize-lines
|
||||
region-id state))
|
||||
(puthash region-id state table)
|
||||
(setq max-offset
|
||||
(max 0 (- (length (plist-get state :content-lines))
|
||||
(plist-get state :content-height))))
|
||||
(unless (> max-offset 0)
|
||||
(error "Scroll evaluator root has no scroll capacity: %s"
|
||||
file))
|
||||
;; Publish warmed content before measuring cached motion.
|
||||
(ebox--scroll-region-by region-id 1 1)
|
||||
(ebox--scroll-region-by region-id -1 1)
|
||||
(dolist (position '(top middle))
|
||||
(ebox--surface-scroll-to-offset
|
||||
buffer region-id
|
||||
(if (eq position 'top) 0 (/ max-offset 2)))
|
||||
(let* ((measurement
|
||||
(ebox-playground-scroll-evaluator--measure
|
||||
region-id steps))
|
||||
(parity
|
||||
(ebox-playground-scroll-evaluator--fresh-parity
|
||||
buffer file))
|
||||
(result
|
||||
(append
|
||||
(list :file (expand-file-name file)
|
||||
:width width :height height :steps steps
|
||||
:position position :max-offset max-offset
|
||||
:fresh-render-parity parity)
|
||||
measurement)))
|
||||
(princ
|
||||
(format
|
||||
"%s scroll %s %s mean=%.3fms p95=%.3fms max=%.3fms roots=%d node-styles=%d style-computations=%d parity=%S\n"
|
||||
(if parity "PASS" "FAIL")
|
||||
(file-name-nondirectory file) position
|
||||
(plist-get measurement :mean-ms)
|
||||
(plist-get measurement :p95-ms)
|
||||
(plist-get measurement :max-ms)
|
||||
(plist-get measurement :root-renders)
|
||||
(plist-get measurement :node-styles)
|
||||
(plist-get measurement :style-computations) parity))
|
||||
(push result results)
|
||||
(unless parity
|
||||
(error "Scroll evaluator fresh-render mismatch: %s %s"
|
||||
file position)))))))
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
(when (timerp ebox--deferred-render-gc-timer)
|
||||
(cancel-timer ebox--deferred-render-gc-timer)))
|
||||
(nreverse results)))
|
||||
|
||||
(defun ebox-playground-flex-resize-evaluator--settle-preview
|
||||
(preview preview-window)
|
||||
"Publish PREVIEW at its settled PREVIEW-WINDOW viewport before measuring.
|
||||
@ -84,9 +611,10 @@ mounted the split. This setup publication is outside the measured splitter
|
||||
action, so the action starts from a self-consistent runtime generation rather
|
||||
than charging initial frame settling to resize."
|
||||
(let* ((width (ebox-playground--window-viewport-width preview-window))
|
||||
(state (and (buffer-live-p preview)
|
||||
(ebox--buffer-render-state preview)))
|
||||
(published-width (and state (plist-get state :viewport-width))))
|
||||
(report (ebox-buffer-update-report preview))
|
||||
(published-width (and report
|
||||
(or (plist-get report :target-viewport-width)
|
||||
(plist-get report :viewport-width)))))
|
||||
(unless (numberp width)
|
||||
(error "flex preview has no settled viewport width"))
|
||||
(when (not (equal width published-width))
|
||||
@ -109,26 +637,37 @@ concurrent cache warming CPU work."
|
||||
(source nil)
|
||||
(preview nil)
|
||||
(preview-window nil)
|
||||
(resize-advice nil)
|
||||
(warmup-publications 0)
|
||||
(resize-count 0)
|
||||
(resize-elapsed 0.0)
|
||||
(resize-finished nil)
|
||||
(action-start nil)
|
||||
(last-event-elapsed nil)
|
||||
(resize-publications nil)
|
||||
(resize-durations nil)
|
||||
(publication-durations nil)
|
||||
(requested-widths nil)
|
||||
(accepted-widths nil)
|
||||
(observer nil)
|
||||
(native-report nil)
|
||||
(playground-library (symbol-file 'ebox-playground-open 'defun))
|
||||
(evaluator-library
|
||||
(symbol-file 'ebox-playground-flex-resize-evaluator-run 'defun))
|
||||
(completed-without-error nil)
|
||||
(ebox-playground-flex-resize-evaluator--output-lines nil)
|
||||
(checks nil))
|
||||
(condition-case-unless-debug error-data
|
||||
(progn
|
||||
(delete-other-windows)
|
||||
(setq native-report (ebox-native-reflow-runtime-report))
|
||||
(unless (plist-get native-report :layout-ready-p)
|
||||
(error "Flex evaluator requires the prepared native layout module"))
|
||||
(unless (file-readable-p
|
||||
ebox-playground-flex-resize-evaluator-fixture)
|
||||
(error "Flex evaluator fixture is not readable: %s"
|
||||
ebox-playground-flex-resize-evaluator-fixture))
|
||||
(setq source
|
||||
(find-file ebox-playground-flex-resize-evaluator--fixture))
|
||||
(find-file ebox-playground-flex-resize-evaluator-fixture))
|
||||
(ebox-dsl-mode)
|
||||
(ebox-dsl-render)
|
||||
(setq preview
|
||||
(ebox-playground-flex-resize-evaluator--preview-buffer)
|
||||
(setq preview (ebox-dsl-render)
|
||||
preview-window (get-buffer-window preview (selected-frame)))
|
||||
(unless (and (buffer-live-p preview)
|
||||
(window-live-p preview-window))
|
||||
@ -139,30 +678,52 @@ concurrent cache warming CPU work."
|
||||
preview preview-window)
|
||||
(redisplay t)
|
||||
(sleep-for 0.2)
|
||||
(setq resize-advice
|
||||
(lambda (original &rest arguments)
|
||||
(setq resize-count (1+ resize-count))
|
||||
(let ((start (float-time)))
|
||||
(unwind-protect
|
||||
(apply original arguments)
|
||||
(let ((duration (- (float-time) start)))
|
||||
(setq resize-elapsed (+ resize-elapsed duration))
|
||||
(push duration resize-durations))
|
||||
(when action-start
|
||||
(push (- (float-time) action-start)
|
||||
resize-publications))
|
||||
(setq resize-finished t)))))
|
||||
(advice-add 'ebox-rerender-buffer-with-context
|
||||
:around resize-advice)
|
||||
;; Observe only accepted, completed Ebox publications. TP emits its
|
||||
;; paired report first; filtering by provider keeps this evaluator on
|
||||
;; the public Ebox boundary and avoids timing/advising the rerender
|
||||
;; implementation itself.
|
||||
(setq observer
|
||||
(lambda (_buffer report)
|
||||
(when (and (eq (plist-get report :provider) 'ebox)
|
||||
(eq (plist-get report :stage) 'viewport))
|
||||
(setq resize-count (1+ resize-count)
|
||||
resize-finished t)
|
||||
(when action-start
|
||||
(push (- (float-time) action-start)
|
||||
resize-publications))
|
||||
(push (/ (or (plist-get report :duration-ms) 0.0)
|
||||
1000.0)
|
||||
publication-durations)
|
||||
(push (or (plist-get report :target-viewport-width)
|
||||
(plist-get report :viewport-width))
|
||||
accepted-widths))))
|
||||
(ebox-buffer-set-observer preview observer)
|
||||
(dotimes (index ebox-playground-flex-resize-evaluator-warmup-count)
|
||||
(window-resize preview-window (if (zerop (% index 2)) -1 1) t)
|
||||
(redisplay t)
|
||||
(sleep-for 0.01))
|
||||
(setq warmup-publications resize-count
|
||||
resize-count 0
|
||||
resize-finished nil
|
||||
resize-publications nil
|
||||
publication-durations nil
|
||||
requested-widths nil
|
||||
accepted-widths nil)
|
||||
(let* ((start (float-time))
|
||||
(old-width
|
||||
(ebox-playground--window-viewport-width preview-window)))
|
||||
(setq action-start start)
|
||||
(dolist (delta '(-4 2 -4 2 -4 2))
|
||||
(setq last-event-elapsed (- (float-time) start))
|
||||
(window-resize preview-window delta t)
|
||||
(redisplay t)
|
||||
(sleep-for 0.02))
|
||||
(dotimes (index ebox-playground-flex-resize-evaluator-sample-count)
|
||||
(let ((delta
|
||||
(if (< index 28)
|
||||
(if (zerop (% index 2)) -1 1)
|
||||
-1)))
|
||||
(setq last-event-elapsed (- (float-time) start))
|
||||
(window-resize preview-window delta t)
|
||||
(push (ebox-playground--window-viewport-width preview-window)
|
||||
requested-widths)
|
||||
(redisplay t)
|
||||
(sleep-for 0.01)))
|
||||
(let* ((elapsed (- (float-time) start))
|
||||
(publication-times
|
||||
(sort (copy-sequence resize-publications) #'<))
|
||||
@ -173,7 +734,14 @@ concurrent cache warming CPU work."
|
||||
(first-publication (car publication-times))
|
||||
(last-publication (car (last publication-times)))
|
||||
(max-publication
|
||||
(if resize-durations (apply #'max resize-durations) 0.0))
|
||||
(if publication-durations
|
||||
(apply #'max publication-durations)
|
||||
0.0))
|
||||
(p95-publication
|
||||
(if publication-durations
|
||||
(ebox-playground-flex-resize-evaluator--percentile
|
||||
publication-durations 0.95)
|
||||
0.0))
|
||||
(max-gap
|
||||
(if publication-gaps (apply #'max publication-gaps) 0.0))
|
||||
(final-latency
|
||||
@ -183,10 +751,8 @@ concurrent cache warming CPU work."
|
||||
(ebox-playground--window-viewport-width preview-window))
|
||||
(report (ebox-buffer-update-report preview))
|
||||
(projection-kind (plist-get report :projection-kind))
|
||||
(native-flex-calls
|
||||
(and (boundp
|
||||
'ebox-native-reflow--flex-geometry-call-count)
|
||||
ebox-native-reflow--flex-geometry-call-count))
|
||||
(expected-widths (nreverse requested-widths))
|
||||
(published-widths (nreverse accepted-widths))
|
||||
(line-widths
|
||||
(ebox-playground-flex-resize-evaluator--line-widths
|
||||
preview))
|
||||
@ -202,16 +768,14 @@ concurrent cache warming CPU work."
|
||||
;; Compare the published visible scroll
|
||||
;; window with a fresh render at the same
|
||||
;; viewport height. Omitting this binding
|
||||
;; rendered the entire lazy document and made
|
||||
;; every correct retained resize look unequal.
|
||||
(ebox-viewport-height
|
||||
(or (plist-get
|
||||
(ebox--buffer-render-state preview)
|
||||
:viewport-height)
|
||||
(or (plist-get report
|
||||
:target-viewport-height)
|
||||
(plist-get report :viewport-height)
|
||||
36)))
|
||||
(ebox-render
|
||||
(ebox-playground-view
|
||||
ebox-playground-flex-resize-evaluator--fixture
|
||||
ebox-playground-flex-resize-evaluator-fixture
|
||||
viewport-width)))))
|
||||
(fresh-render-matches-p
|
||||
(and (stringp fresh-rendered)
|
||||
@ -230,24 +794,55 @@ concurrent cache warming CPU work."
|
||||
(not (plist-get report :tp-scope-fallback)))))
|
||||
(setq checks
|
||||
(list
|
||||
(ebox-playground-flex-resize-evaluator--check
|
||||
"compiled evaluator artifacts"
|
||||
(and (stringp playground-library)
|
||||
(string-suffix-p ".elc" playground-library)
|
||||
(stringp evaluator-library)
|
||||
(string-suffix-p ".elc" evaluator-library))
|
||||
(format "playground=%S evaluator=%S"
|
||||
playground-library evaluator-library))
|
||||
(ebox-playground-flex-resize-evaluator--check
|
||||
"native module prepared"
|
||||
(plist-get native-report :layout-ready-p)
|
||||
(format "path=%S hash=%S"
|
||||
(plist-get native-report :loaded-module-path)
|
||||
(plist-get native-report :loaded-module-hash)))
|
||||
(ebox-playground-flex-resize-evaluator--check
|
||||
"preview mounted"
|
||||
(and (buffer-live-p preview)
|
||||
(window-live-p preview-window))
|
||||
(format "source=%S preview=%S" source preview))
|
||||
(ebox-playground-flex-resize-evaluator--check
|
||||
"warmup publications"
|
||||
(= warmup-publications
|
||||
ebox-playground-flex-resize-evaluator-warmup-count)
|
||||
(format "warmups=%d expected=%d"
|
||||
warmup-publications
|
||||
ebox-playground-flex-resize-evaluator-warmup-count))
|
||||
(ebox-playground-flex-resize-evaluator--check
|
||||
"intermediate resize updates"
|
||||
(and (>= resize-count 4)
|
||||
(and (= resize-count
|
||||
ebox-playground-flex-resize-evaluator-sample-count)
|
||||
(= resize-count (length expected-widths))
|
||||
first-publication
|
||||
(<= first-publication
|
||||
ebox-playground-flex-resize-evaluator-max-update-seconds)
|
||||
(< first-publication last-event-elapsed))
|
||||
(format "updates=%d first=%.6fs last-event=%.6fs"
|
||||
(format "updates=%d samples=%d first=%.6fs last-event=%.6fs"
|
||||
resize-count
|
||||
ebox-playground-flex-resize-evaluator-sample-count
|
||||
(or first-publication 0.0)
|
||||
last-event-elapsed))
|
||||
(ebox-playground-flex-resize-evaluator--check
|
||||
"per-update resize budget"
|
||||
"per-update p95 budget"
|
||||
(<= p95-publication
|
||||
ebox-playground-flex-resize-evaluator-max-update-seconds)
|
||||
(format "p95=%.6fs threshold=%.6fs"
|
||||
p95-publication
|
||||
ebox-playground-flex-resize-evaluator-max-update-seconds))
|
||||
(ebox-playground-flex-resize-evaluator--check
|
||||
"per-update max budget"
|
||||
(<= max-publication
|
||||
ebox-playground-flex-resize-evaluator-max-update-seconds)
|
||||
(format "max=%.6fs threshold=%.6fs"
|
||||
@ -277,11 +872,15 @@ concurrent cache warming CPU work."
|
||||
"viewport was published"
|
||||
(and (numberp viewport-width)
|
||||
(not (equal old-width viewport-width))
|
||||
(cl-every #'numberp published-widths)
|
||||
(equal expected-widths published-widths)
|
||||
(equal viewport-width (car (last published-widths)))
|
||||
(eq (plist-get report :constraint-source)
|
||||
'viewport)
|
||||
(plist-get report :runtime-published))
|
||||
(format "viewport=%S strategy=%S"
|
||||
viewport-width (plist-get report :strategy)))
|
||||
(format "viewport=%S expected=%S accepted=%S strategy=%S"
|
||||
viewport-width expected-widths published-widths
|
||||
(plist-get report :strategy)))
|
||||
(ebox-playground-flex-resize-evaluator--check
|
||||
"retained viewport projection"
|
||||
projection-retained-p
|
||||
@ -304,21 +903,22 @@ concurrent cache warming CPU work."
|
||||
'((default (:background "#FAF7F0"))) )
|
||||
(format "face-remapping=%S" background))
|
||||
(ebox-playground-flex-resize-evaluator--check
|
||||
"resize is millisecond-scale"
|
||||
"bounded resize burst"
|
||||
(<= elapsed
|
||||
ebox-playground-flex-resize-evaluator-max-seconds)
|
||||
(format "elapsed=%.6fs threshold=%.6fs"
|
||||
elapsed
|
||||
ebox-playground-flex-resize-evaluator-max-seconds))))
|
||||
(push (format
|
||||
"RESIZE old-viewport-px=%S new-viewport-px=%S total=%.6fs rerender=%.6fs first=%.6fs max-update=%.6fs max-gap=%.6fs final=%.6fs strategy=%S projection=%S axes=%S reconciled=%S native-flex=%S tp-ops=%S cache-hits=%S cache-misses=%S"
|
||||
old-width viewport-width elapsed resize-elapsed
|
||||
(or first-publication 0.0) max-publication max-gap
|
||||
"RESIZE warmups=%d samples=%d old-viewport-px=%S new-viewport-px=%S total=%.6fs p95-update=%.6fs max-update=%.6fs first=%.6fs max-gap=%.6fs final=%.6fs strategy=%S projection=%S axes=%S reconciled=%S tp-ops=%S cache-hits=%S cache-misses=%S"
|
||||
warmup-publications resize-count old-width viewport-width
|
||||
elapsed p95-publication max-publication
|
||||
(or first-publication 0.0) max-gap
|
||||
(or final-latency -1.0)
|
||||
(plist-get report :strategy) projection-kind
|
||||
(plist-get report :viewport-axes)
|
||||
(plist-get report :reconciled-objects)
|
||||
native-flex-calls tp-operations
|
||||
tp-operations
|
||||
(plist-get report :cache-hit-count)
|
||||
(plist-get report :cache-miss-count))
|
||||
ebox-playground-flex-resize-evaluator--output-lines)
|
||||
@ -333,8 +933,8 @@ concurrent cache warming CPU work."
|
||||
(push "FAIL evaluator interrupted"
|
||||
ebox-playground-flex-resize-evaluator--output-lines)
|
||||
nil))
|
||||
(when resize-advice
|
||||
(advice-remove 'ebox-rerender-buffer-with-context resize-advice))
|
||||
(when (and (buffer-live-p preview) observer)
|
||||
(ebox-buffer-set-observer preview nil))
|
||||
(when (buffer-live-p preview)
|
||||
(kill-buffer preview))
|
||||
(when (buffer-live-p source)
|
||||
|
||||
@ -5,12 +5,42 @@ root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
emacs=${EMACS:-/Applications/Emacs.app/Contents/MacOS/Emacs}
|
||||
emacsclient=${EMACSCLIENT:-/Applications/Emacs.app/Contents/MacOS/bin/emacsclient}
|
||||
server="ebox-playground-flex-evaluator-$$"
|
||||
fixture=$(mktemp "${TMPDIR:-/tmp}/ebox-flex-reference.XXXXXX")
|
||||
|
||||
require_fresh_elc() {
|
||||
source_file=$1
|
||||
elc_file=$2
|
||||
if [ ! -r "$elc_file" ] || [ "$source_file" -nt "$elc_file" ]; then
|
||||
echo "stale or missing evaluator artifact: $elc_file" >&2
|
||||
echo "run make performance-prepare first" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
"$emacsclient" -n -s "$server" -e '(kill-emacs 0)' >/dev/null 2>&1 || true
|
||||
rm -f "$fixture"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
require_fresh_elc "$root/ebox-playground.el" "$root/ebox-playground.elc"
|
||||
require_fresh_elc \
|
||||
"$root/scripts/ebox-playground-flex-resize-evaluator.el" \
|
||||
"$root/scripts/ebox-playground-flex-resize-evaluator.elc"
|
||||
require_fresh_elc "$root/../ebox/ebox.el" "$root/../ebox/ebox.elc"
|
||||
require_fresh_elc "$root/../ebox/ebox-surface.el" "$root/../ebox/ebox-surface.elc"
|
||||
require_fresh_elc "$root/../ecss/ecss.el" "$root/../ecss/ecss.elc"
|
||||
require_fresh_elc "$root/../tp/tp.el" "$root/../tp/tp.elc"
|
||||
|
||||
: "${EBOX_NATIVE_REFLOW_MODULE_PATH:?run make performance-prepare first}"
|
||||
if [ ! -r "$EBOX_NATIVE_REFLOW_MODULE_PATH/libebox_native_reflow.dylib" ]; then
|
||||
echo "prepared Ebox native module is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git -C "$root" show HEAD:examples/flex-reference.ebox >"$fixture"
|
||||
export EBOX_PLAYGROUND_FLEX_FIXTURE=$fixture
|
||||
|
||||
# Keep the evaluator package-only, but reproduce only the four startup values
|
||||
# that directly affect this render path. Loading the complete user init would
|
||||
# add unrelated packages, timers, and UI state to the performance gate.
|
||||
@ -18,12 +48,15 @@ trap cleanup EXIT INT TERM
|
||||
-L "$root" -L "$root/../ebox" -L "$root/../ecss" \
|
||||
-L "$root/../tp" \
|
||||
--eval '(setq load-prefer-newer t
|
||||
native-comp-jit-compilation nil
|
||||
ebox-native-reflow-module-path
|
||||
(getenv "EBOX_NATIVE_REFLOW_MODULE_PATH")
|
||||
gc-cons-threshold most-positive-fixnum
|
||||
gc-cons-percentage 0.6
|
||||
file-name-handler-alist nil
|
||||
auto-window-vscroll nil)' \
|
||||
-l "$root/ebox-playground.el" \
|
||||
-l "$root/scripts/ebox-playground-flex-resize-evaluator.el"
|
||||
-l "$root/ebox-playground" \
|
||||
-l "$root/scripts/ebox-playground-flex-resize-evaluator"
|
||||
|
||||
output=$("$emacsclient" -n -s "$server" -c -e \
|
||||
'(ebox-playground-flex-resize-evaluator-run)' 2>&1)
|
||||
|
||||
525
tests/ebox-playground-interaction-tests.el
Normal file
525
tests/ebox-playground-interaction-tests.el
Normal file
@ -0,0 +1,525 @@
|
||||
;;; ebox-playground-interaction-tests.el --- Native interaction example tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
;; Exercise the actual example through the generic public file runner. Native
|
||||
;; properties and commands are observed on rendered text; lexical closures and
|
||||
;; keymap graphs are intentionally treated as opaque application values.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'cl-lib)
|
||||
(require 'ebox-playground)
|
||||
(require 'ebox-playground-flex-resize-evaluator
|
||||
(expand-file-name "scripts/ebox-playground-flex-resize-evaluator.el"
|
||||
ebox-playground-directory))
|
||||
|
||||
(defconst ebox-playground-interaction-test--file
|
||||
(expand-file-name "examples/interaction-reference.ebox"
|
||||
ebox-playground-directory)
|
||||
"Interaction example checked through the ordinary file runner.")
|
||||
|
||||
(cl-defmacro ebox-playground-interaction-test--with-preview
|
||||
((buffer &optional (width 720) (height 200)) &rest body)
|
||||
"Run BODY in a fresh BUFFER preview at WIDTH and HEIGHT, then close it."
|
||||
(declare (indent 1) (debug ((symbolp &optional form form) body)))
|
||||
`(let ((,buffer (generate-new-buffer " *ebox-interaction-test*"))
|
||||
(ebox-viewport-width ,width)
|
||||
(ebox-viewport-height ,height)
|
||||
(ebox-runtime-idle-prewarm nil)
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-playground-open-file
|
||||
ebox-playground-interaction-test--file (buffer-name ,buffer))
|
||||
(with-current-buffer ,buffer ,@body))
|
||||
(when (buffer-live-p ,buffer)
|
||||
(ebox-playground-close (buffer-name ,buffer))))))
|
||||
|
||||
(defun ebox-playground-interaction-test--position (label)
|
||||
"Return the first rendered character position of LABEL in this preview."
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(unless (search-forward label nil t)
|
||||
(ert-fail (format "Missing rendered interaction label: %s" label)))
|
||||
(- (point) (length label))))
|
||||
|
||||
(defun ebox-playground-interaction-test--property (label property)
|
||||
"Return native PROPERTY at the rendered LABEL."
|
||||
(get-text-property
|
||||
(ebox-playground-interaction-test--position label) property))
|
||||
|
||||
(defun ebox-playground-interaction-test--activate (label &optional key)
|
||||
"Look up KEY at rendered LABEL and invoke its native interactive command."
|
||||
(let* ((keys (or key (kbd "RET")))
|
||||
(position (ebox-playground-interaction-test--position label))
|
||||
(map (get-text-property position 'keymap)))
|
||||
(goto-char position)
|
||||
(should (keymapp map))
|
||||
(let ((command (key-binding keys))
|
||||
(last-input-event (aref (vconcat keys) (1- (length keys)))))
|
||||
(should (commandp command))
|
||||
(should (eq command (lookup-key map keys)))
|
||||
(call-interactively command)
|
||||
command)))
|
||||
|
||||
(defun ebox-playground-interaction-test--click (label &optional offset)
|
||||
"Click rendered LABEL at OFFSET through native event lookup in another buffer."
|
||||
(let* ((buffer (current-buffer))
|
||||
(position (+ (ebox-playground-interaction-test--position label)
|
||||
(or offset 0)))
|
||||
(map (get-text-property position 'keymap))
|
||||
command)
|
||||
(should (keymapp map))
|
||||
(save-window-excursion
|
||||
(set-window-buffer (selected-window) buffer)
|
||||
(let* ((event (list 'mouse-1 (list (selected-window) position '(0 . 0) 0)))
|
||||
(keys (vector event))
|
||||
(last-input-event event))
|
||||
(setq command (key-binding [mouse-1] nil nil position))
|
||||
(should (commandp command))
|
||||
(should (eq command (lookup-key map [mouse-1])))
|
||||
(with-temp-buffer
|
||||
(call-interactively command nil keys)
|
||||
(should (zerop (buffer-size))))))
|
||||
command))
|
||||
|
||||
(defun ebox-playground-interaction-test--count (expected)
|
||||
"Assert that the preview displays the independent count EXPECTED."
|
||||
(should (integerp
|
||||
(ebox-playground-interaction-test--position
|
||||
(format "COUNT %02d" expected)))))
|
||||
|
||||
(defun ebox-playground-interaction-test--help ()
|
||||
"Invoke the live action's native help callback with Emacs's arguments."
|
||||
(let* ((position (ebox-playground-interaction-test--position "ADD "))
|
||||
(callback (get-text-property position 'help-echo)))
|
||||
(should (functionp callback))
|
||||
(funcall callback (selected-window) (current-buffer) position)))
|
||||
|
||||
(defun ebox-playground-interaction-test--padding-position (label)
|
||||
"Find LABEL's command on the otherwise blank Box padding line above it."
|
||||
(let* ((position (ebox-playground-interaction-test--position label))
|
||||
(command (lookup-key (get-text-property position 'keymap) (kbd "RET")))
|
||||
(background
|
||||
(plist-get (ebox-playground-flex-resize-evaluator--effective-face
|
||||
(get-text-property position 'face))
|
||||
:background)))
|
||||
(save-excursion
|
||||
(goto-char position)
|
||||
(forward-line -1)
|
||||
(should (string-match-p
|
||||
"\\`[[:space:]]*\\'"
|
||||
(buffer-substring-no-properties
|
||||
(line-beginning-position) (line-end-position))))
|
||||
(or (cl-loop for at from (line-beginning-position)
|
||||
below (line-end-position)
|
||||
for map = (get-text-property at 'keymap)
|
||||
when (and (keymapp map)
|
||||
(eq (lookup-key map (kbd "RET")) command)
|
||||
;; Border glyphs share the Box map but retain
|
||||
;; their stroke color when the interior hovers.
|
||||
(equal
|
||||
(plist-get
|
||||
(ebox-playground-flex-resize-evaluator--effective-face
|
||||
(get-text-property at 'face))
|
||||
:background)
|
||||
background))
|
||||
return at)
|
||||
(ert-fail (format "Box padding lost the command for %s" label))))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-static-help-pointer-and-hover-samples ()
|
||||
"The isolated examples publish native properties on the intended content."
|
||||
(ebox-playground-interaction-test--with-preview (buffer)
|
||||
(dolist (sample '(("ARROW" arrow) ("Text-only hint" text)
|
||||
("HORIZONTAL DRAG" hdrag)))
|
||||
(should (eq (ebox-playground-interaction-test--property
|
||||
(car sample) 'pointer)
|
||||
(cadr sample)))
|
||||
(should (stringp (ebox-playground-interaction-test--property
|
||||
(car sample) 'help-echo)))
|
||||
(should-not (ebox-playground-interaction-test--property
|
||||
(car sample) 'keymap)))
|
||||
(should-not (ebox-playground-interaction-test--property
|
||||
"TEXT POINTER" 'pointer))
|
||||
(should-not (ebox-playground-interaction-test--property
|
||||
"TEXT POINTER" 'help-echo))
|
||||
(let ((cool (ebox-playground-flex-resize-evaluator--effective-face
|
||||
(ebox-playground-interaction-test--property
|
||||
"COOL FILL" 'mouse-face)))
|
||||
(warm (ebox-playground-flex-resize-evaluator--effective-face
|
||||
(ebox-playground-interaction-test--property
|
||||
"WARM + UNDERLINE" 'mouse-face))))
|
||||
(should (equal (plist-get cool :foreground) "#FFFFFF"))
|
||||
(should (equal (plist-get cool :background) "#286477"))
|
||||
(should (equal (plist-get warm :foreground) "#683D16"))
|
||||
(should (equal (plist-get warm :background) "#F6D6AB"))
|
||||
(should (plist-get warm :underline)))
|
||||
(should-not (ebox-playground-interaction-test--property
|
||||
"NO HOVER STYLE" 'mouse-face))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-native-keys-replace-step-and-read-live-help ()
|
||||
"Real key lookup uses a replaced +5 map and callbacks see current state."
|
||||
(ebox-playground-interaction-test--with-preview (buffer)
|
||||
(ebox-playground-interaction-test--count 0)
|
||||
(should (string-match-p "count = 0; this action adds 1"
|
||||
(ebox-playground-interaction-test--help)))
|
||||
(let ((first-command
|
||||
(ebox-playground-interaction-test--activate "ADD 1")))
|
||||
(ebox-playground-interaction-test--count 1)
|
||||
(should (string-match-p "count = 1; this action adds 1"
|
||||
(ebox-playground-interaction-test--help)))
|
||||
(ebox-playground-interaction-test--activate "SWITCH STEP" (kbd "SPC"))
|
||||
(should (string-match-p "count = 1; this action adds 5"
|
||||
(ebox-playground-interaction-test--help)))
|
||||
(should-not
|
||||
(eq first-command
|
||||
(ebox-playground-interaction-test--activate "ADD 5" [return])))
|
||||
(ebox-playground-interaction-test--count 6))
|
||||
(ebox-playground-interaction-test--activate "ADD 5" (kbd "+"))
|
||||
(ebox-playground-interaction-test--count 11)
|
||||
(ebox-playground-interaction-test--activate "ADD 5" (kbd "-"))
|
||||
(ebox-playground-interaction-test--count 6)
|
||||
(should (string-match-p "count = 6; this action adds 5"
|
||||
(ebox-playground-interaction-test--help)))
|
||||
(should (integerp (ebox-playground-interaction-test--position
|
||||
"-5 → count 6")))
|
||||
(should (ebox-buffer-update-report buffer))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-minus-click-captures-step-and-restores ()
|
||||
"Clicking the minus glyph subtracts, while the enclosing ADD surface adds."
|
||||
(ebox-playground-interaction-test--with-preview (buffer)
|
||||
(let ((minus-one (ebox-playground-interaction-test--click "[-]" 1)))
|
||||
(ebox-playground-interaction-test--count -1)
|
||||
(let ((add-one (ebox-playground-interaction-test--click "ADD 1")))
|
||||
(should-not (eq minus-one add-one))
|
||||
(should (eq add-one
|
||||
(lookup-key
|
||||
(get-text-property
|
||||
(ebox-playground-interaction-test--padding-position "ADD 1")
|
||||
'keymap)
|
||||
[mouse-1]))))
|
||||
(ebox-playground-interaction-test--count 0)
|
||||
(ebox-playground-interaction-test--activate "[-]" (kbd "+"))
|
||||
(ebox-playground-interaction-test--count 1)
|
||||
(ebox-playground-interaction-test--activate "[-]" (kbd "-"))
|
||||
(ebox-playground-interaction-test--count 0)
|
||||
(ebox-playground-interaction-test--activate "SWITCH STEP")
|
||||
(should (integerp (ebox-playground-interaction-test--position
|
||||
"ADD 5 [+] [-]")))
|
||||
(let ((minus-five (ebox-playground-interaction-test--click "[-]" 1)))
|
||||
(should-not (eq minus-one minus-five))
|
||||
(ebox-playground-interaction-test--count -5)
|
||||
(let ((position (1+ (ebox-playground-interaction-test--position "[-]"))))
|
||||
(should (string-match-p
|
||||
"count = -5; this action subtracts 5"
|
||||
(funcall (get-text-property position 'help-echo)
|
||||
(selected-window) buffer position))))
|
||||
(let ((add-five (ebox-playground-interaction-test--click "ADD 5")))
|
||||
(should-not (eq add-five minus-five))
|
||||
(ebox-playground-interaction-test--count 0)
|
||||
(ebox-playground-interaction-test--activate "[-]" (kbd "+"))
|
||||
(ebox-playground-interaction-test--count 5)
|
||||
(ebox-playground-interaction-test--activate "[-]" (kbd "-"))
|
||||
(ebox-playground-interaction-test--count 0)
|
||||
(ebox-playground-interaction-test--activate "REMOVE / RESTORE")
|
||||
(should (integerp (ebox-playground-interaction-test--position
|
||||
"BINDING REMOVED [-]")))
|
||||
(let ((position (1+ (ebox-playground-interaction-test--position "[-]"))))
|
||||
(dolist (property '(keymap help-echo mouse-face pointer))
|
||||
(should-not (get-text-property position property)))
|
||||
(save-window-excursion
|
||||
(set-window-buffer (selected-window) buffer)
|
||||
(let* ((event (list 'mouse-1
|
||||
(list (selected-window) position '(0 . 0) 0)))
|
||||
(binding (key-binding [mouse-1] nil nil
|
||||
(posn-point (event-start event)))))
|
||||
(should-not (memq binding (list minus-five add-five))))))
|
||||
(ebox-playground-interaction-test--count 0)
|
||||
(ebox-playground-interaction-test--activate "REMOVE / RESTORE")
|
||||
(should (integerp (ebox-playground-interaction-test--position
|
||||
"ADD 5 [+] [-]")))
|
||||
(dolist (property '(keymap help-echo mouse-face pointer))
|
||||
(should (ebox-playground-interaction-test--property "[-]" property)))
|
||||
(ebox-playground-interaction-test--click "[-]" 1)
|
||||
(ebox-playground-interaction-test--count -5)
|
||||
(ebox-playground-interaction-test--click "ADD 5")
|
||||
(ebox-playground-interaction-test--count 0))))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-class-control-updates-all-statuses-in-one-batch ()
|
||||
"One native command updates three class targets and preserves other controls."
|
||||
(ebox-playground-interaction-test--with-preview (buffer)
|
||||
(should (= (length (ebox-selector-query-buffer buffer ".demo-status")) 3))
|
||||
(dolist (label '("WAIT A" "WAIT B" "WAIT C"))
|
||||
(should (equal
|
||||
(plist-get
|
||||
(ebox-playground-flex-resize-evaluator--effective-face
|
||||
(ebox-playground-interaction-test--property label 'face))
|
||||
:foreground)
|
||||
"#85652F"))
|
||||
(should-not (ebox-playground-interaction-test--property label 'help-echo)))
|
||||
(let* ((count-command
|
||||
(ebox-playground-interaction-test--activate "ADD 1"))
|
||||
(revision (plist-get (ebox-buffer-update-report buffer)
|
||||
:runtime-revision)))
|
||||
(should (integerp revision))
|
||||
(ebox-playground-interaction-test--activate "UPDATE STATUS GROUP" (kbd "SPC"))
|
||||
;; The selector batch publishes once; recording the event publishes once.
|
||||
(should (= (plist-get (ebox-buffer-update-report buffer) :runtime-revision)
|
||||
(+ revision 2)))
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(let ((ready-count 0))
|
||||
(while (search-forward "READY" nil t)
|
||||
(cl-incf ready-count)
|
||||
(let ((position (- (point) (length "READY"))))
|
||||
(should (equal (get-text-property position 'help-echo)
|
||||
"This status was updated with its class group."))
|
||||
(should (equal
|
||||
(plist-get
|
||||
(ebox-playground-flex-resize-evaluator--effective-face
|
||||
(get-text-property position 'face))
|
||||
:foreground)
|
||||
"#287A52"))))
|
||||
(should (= ready-count 3)))
|
||||
(goto-char (point-min))
|
||||
(should-not (re-search-forward "WAIT [ABC]" nil t)))
|
||||
(should (integerp (ebox-playground-interaction-test--position
|
||||
"Updated three .demo-status regions in one batch")))
|
||||
(ebox-playground-interaction-test--count 1)
|
||||
(should (eq count-command
|
||||
(ebox-playground-interaction-test--activate "ADD 1")))
|
||||
(ebox-playground-interaction-test--count 2)
|
||||
(ebox-playground-interaction-test--activate "[ RESET COUNT ]")
|
||||
(ebox-playground-interaction-test--count 0))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-removes-and-restores-all-four-properties ()
|
||||
"Removing bindings clears every native property on content and padding."
|
||||
(ebox-playground-interaction-test--with-preview (buffer)
|
||||
(ebox-playground-interaction-test--activate "SWITCH STEP")
|
||||
(let ((before (mapcar
|
||||
(lambda (property)
|
||||
(cons property
|
||||
(ebox-playground-interaction-test--property
|
||||
"ADD 5" property)))
|
||||
'(help-echo pointer mouse-face keymap)))
|
||||
(padding (ebox-playground-interaction-test--padding-position "ADD 5")))
|
||||
(dolist (property '(help-echo pointer mouse-face keymap))
|
||||
(should (get-text-property padding property)))
|
||||
(let ((padding-offset (save-excursion
|
||||
(goto-char padding)
|
||||
(- padding (line-beginning-position)))))
|
||||
(ebox-playground-interaction-test--activate "REMOVE / RESTORE")
|
||||
;; The status above this row changes length, so find the new padding
|
||||
;; position instead of accidentally testing an old buffer offset.
|
||||
(setq padding
|
||||
(save-excursion
|
||||
(goto-char (ebox-playground-interaction-test--position
|
||||
"BINDING REMOVED"))
|
||||
(forward-line -1)
|
||||
(+ (line-beginning-position) padding-offset))))
|
||||
(let ((position (ebox-playground-interaction-test--position
|
||||
"BINDING REMOVED")))
|
||||
(goto-char position)
|
||||
(dolist (property '(help-echo pointer mouse-face keymap))
|
||||
(should-not (get-text-property position property))
|
||||
(should-not (get-text-property padding property)))
|
||||
(should-not
|
||||
(eq (key-binding (kbd "RET"))
|
||||
(lookup-key (cdr (assq 'keymap before)) (kbd "RET")))))
|
||||
(ebox-playground-interaction-test--count 0)
|
||||
(ebox-playground-interaction-test--activate "REMOVE / RESTORE")
|
||||
(dolist (property '(help-echo pointer mouse-face keymap))
|
||||
(should (ebox-playground-interaction-test--property "ADD 5" property)))
|
||||
(should (string-match-p "count = 0; this action adds 5"
|
||||
(ebox-playground-interaction-test--help)))
|
||||
(should (equal (ebox-playground-interaction-test--property
|
||||
"ADD 5" 'mouse-face)
|
||||
(cdr (assq 'mouse-face before))))
|
||||
(ebox-playground-interaction-test--activate "ADD 5")
|
||||
(ebox-playground-interaction-test--count 5))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-palette-update-preserves-geometry-and-command ()
|
||||
"Hover replacement publishes native faces without moving the live control."
|
||||
(ebox-playground-interaction-test--with-preview (buffer)
|
||||
(let* ((position (ebox-playground-interaction-test--position "ADD 1"))
|
||||
(padding (ebox-playground-interaction-test--padding-position "ADD 1"))
|
||||
(map (get-text-property position 'keymap))
|
||||
(command (lookup-key map (kbd "RET")))
|
||||
(line-width (save-excursion
|
||||
(goto-char position)
|
||||
(ebox-string-pixel-width
|
||||
(buffer-substring (line-beginning-position)
|
||||
(line-end-position))))))
|
||||
(should (equal (plist-get
|
||||
(ebox-playground-flex-resize-evaluator--effective-face
|
||||
(get-text-property position 'mouse-face))
|
||||
:background)
|
||||
"#BCDDE5"))
|
||||
(ebox-playground-interaction-test--activate "SWITCH HOVER")
|
||||
(should (= position (ebox-playground-interaction-test--position "ADD 1")))
|
||||
(dolist (at (list position padding))
|
||||
(let ((face (ebox-playground-flex-resize-evaluator--effective-face
|
||||
(get-text-property at 'mouse-face))))
|
||||
(should (equal (plist-get face :background) "#F6D6AB"))
|
||||
(should (equal (plist-get face :foreground) "#683D16"))
|
||||
(should (plist-get face :underline))))
|
||||
(save-excursion
|
||||
(goto-char position)
|
||||
(should (= line-width
|
||||
(ebox-string-pixel-width
|
||||
(buffer-substring (line-beginning-position)
|
||||
(line-end-position))))))
|
||||
(should (eq command
|
||||
(ebox-playground-interaction-test--activate "ADD 1")))
|
||||
(ebox-playground-interaction-test--count 1)
|
||||
(ebox-playground-interaction-test--activate "SWITCH HOVER")
|
||||
(should (equal
|
||||
(plist-get
|
||||
(ebox-playground-flex-resize-evaluator--effective-face
|
||||
(ebox-playground-interaction-test--property "ADD 1" 'mouse-face))
|
||||
:background)
|
||||
"#BCDDE5")))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-text-keymap-resets-count ()
|
||||
"A Text node dispatches its own native command inside ordinary prose."
|
||||
(ebox-playground-interaction-test--with-preview (buffer)
|
||||
(ebox-playground-interaction-test--activate "ADD 1")
|
||||
(ebox-playground-interaction-test--activate "ADD 1" (kbd "SPC"))
|
||||
(ebox-playground-interaction-test--count 2)
|
||||
(should-not (ebox-playground-interaction-test--property
|
||||
"Text can be interactive too:" 'keymap))
|
||||
(should (eq (ebox-playground-interaction-test--property
|
||||
"[ RESET COUNT ]" 'pointer) 'hand))
|
||||
(ebox-playground-interaction-test--activate "[ RESET COUNT ]" (kbd "SPC"))
|
||||
(ebox-playground-interaction-test--count 0)
|
||||
(should (integerp (ebox-playground-interaction-test--position "Reset count")))
|
||||
(should (string-match-p "count = 0; this action adds 1"
|
||||
(ebox-playground-interaction-test--help)))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-nested-owner-and-explicit-nil ()
|
||||
"Parent padding works, child bindings win, and explicit nil blocks all four."
|
||||
(ebox-playground-interaction-test--with-preview (buffer)
|
||||
(let ((parent-command
|
||||
(ebox-playground-interaction-test--activate "PARENT SURFACE —")))
|
||||
(should (integerp (ebox-playground-interaction-test--position
|
||||
"01 PARENT surface activated")))
|
||||
(let* ((padding (ebox-playground-interaction-test--padding-position
|
||||
"PARENT SURFACE —"))
|
||||
(last-input-event ?\r))
|
||||
(goto-char padding)
|
||||
(should (eq (key-binding (kbd "RET")) parent-command))
|
||||
(call-interactively (key-binding (kbd "RET"))))
|
||||
(should (integerp (ebox-playground-interaction-test--position
|
||||
"02 PARENT surface activated")))
|
||||
(let ((child-command
|
||||
(ebox-playground-interaction-test--activate "[ CHILD ACTION ]")))
|
||||
;; Activation starts on the child's first character, including the
|
||||
;; key-binding check against its own map in the shared helper.
|
||||
(should-not (eq parent-command child-command))
|
||||
(should (integerp (ebox-playground-interaction-test--position
|
||||
"03 CHILD text activated")))
|
||||
(goto-char (+ (ebox-playground-interaction-test--position
|
||||
"[ CHILD ACTION ]")
|
||||
(length "[ CHILD ACTION ]")))
|
||||
(should (eq (key-binding (kbd "RET")) parent-command))
|
||||
(let* ((label "[ EXPLICIT NIL: no inherited interaction ]")
|
||||
(quiet (ebox-playground-interaction-test--position label)))
|
||||
(dolist (at (list quiet (1+ quiet) (+ quiet (1- (length label)))))
|
||||
(dolist (property '(help-echo pointer mouse-face keymap))
|
||||
(should-not (get-text-property at property)))
|
||||
(goto-char at)
|
||||
(should-not (eq (key-binding (kbd "RET")) parent-command))
|
||||
(should-not (eq (key-binding (kbd "RET")) child-command)))
|
||||
(goto-char (+ quiet (length label)))
|
||||
(should (eq (key-binding (kbd "RET")) parent-command)))))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-box-end-boundaries-exclude-margins ()
|
||||
"A real example's Box command stops before both outside margin characters."
|
||||
(ebox-playground-interaction-test--with-preview (buffer)
|
||||
(ebox-region-update (ebox-region-resolve buffer "parent-surface")
|
||||
:margin-inline '(ch 2))
|
||||
(let* ((position (ebox-playground-interaction-test--position
|
||||
"PARENT SURFACE —"))
|
||||
(command (lookup-key (get-text-property position 'keymap)
|
||||
(kbd "RET")))
|
||||
(start (previous-single-property-change
|
||||
(1+ position) 'keymap nil
|
||||
(save-excursion (goto-char position)
|
||||
(line-beginning-position))))
|
||||
(end (next-single-property-change
|
||||
position 'keymap nil
|
||||
(save-excursion (goto-char position) (line-end-position)))))
|
||||
(should (> start (save-excursion
|
||||
(goto-char position) (line-beginning-position))))
|
||||
(dolist (at (list start (1- end)))
|
||||
(goto-char at)
|
||||
(should (eq (key-binding (kbd "RET")) command)))
|
||||
(dolist (at (list (1- start) end))
|
||||
(goto-char at)
|
||||
(dolist (property '(help-echo pointer mouse-face keymap))
|
||||
(should-not (get-text-property at property)))
|
||||
(should-not (eq (key-binding (kbd "RET")) command))))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-mouse-command-uses-event-buffer ()
|
||||
"A native click targets its window even when another buffer is current."
|
||||
(ebox-playground-interaction-test--with-preview (buffer)
|
||||
(ebox-playground-interaction-test--click "ADD 1")
|
||||
(ebox-playground-interaction-test--count 1)))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-previews-have-independent-state ()
|
||||
"Separate evaluations own independent counts, maps, help, and event logs."
|
||||
(ebox-playground-interaction-test--with-preview (first)
|
||||
(ebox-playground-interaction-test--with-preview (second)
|
||||
(with-current-buffer first
|
||||
(ebox-playground-interaction-test--activate "SWITCH STEP")
|
||||
(ebox-playground-interaction-test--activate "ADD 5")
|
||||
(ebox-playground-interaction-test--count 5))
|
||||
(ebox-playground-interaction-test--count 0)
|
||||
(should (string-match-p "count = 0; this action adds 1"
|
||||
(ebox-playground-interaction-test--help)))
|
||||
(should (integerp (ebox-playground-interaction-test--position
|
||||
"No actions yet.")))
|
||||
(ebox-playground-interaction-test--activate "ADD 1")
|
||||
(ebox-playground-interaction-test--count 1)
|
||||
(with-current-buffer first
|
||||
(ebox-playground-interaction-test--count 5)
|
||||
(should (string-match-p "count = 5; this action adds 5"
|
||||
(ebox-playground-interaction-test--help)))
|
||||
(ebox-playground-interaction-test--activate "[ RESET COUNT ]")
|
||||
(ebox-playground-interaction-test--count 0))
|
||||
(ebox-playground-interaction-test--count 1))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-viewport-resize-retains-updated-binding ()
|
||||
"Both viewport axes change while retained command state remains usable."
|
||||
(ebox-playground-interaction-test--with-preview (buffer)
|
||||
(ebox-playground-interaction-test--activate "SWITCH STEP")
|
||||
(ebox-playground-interaction-test--activate "SWITCH HOVER")
|
||||
(let ((command (ebox-playground-interaction-test--activate "ADD 5"))
|
||||
(expected 5)
|
||||
(original-lines (count-lines (point-min) (point-max))))
|
||||
(dolist (size '((480 220) (1000 180) (720 200)))
|
||||
(ebox-rerender-buffer-with-context buffer (car size) (cadr size))
|
||||
(let ((widths (mapcar #'ebox-string-pixel-width
|
||||
(ebox-string-lines (buffer-string)))))
|
||||
(should (= (apply #'max widths) (car size))))
|
||||
(if (= (cadr size) 200)
|
||||
(should (= (count-lines (point-min) (point-max)) original-lines))
|
||||
(should-not (= (count-lines (point-min) (point-max)) original-lines)))
|
||||
(should (eq command
|
||||
(ebox-playground-interaction-test--activate "ADD 5")))
|
||||
(cl-incf expected 5)
|
||||
(ebox-playground-interaction-test--count expected)
|
||||
(should (string-match-p
|
||||
(format "count = %d; this action adds 5" expected)
|
||||
(ebox-playground-interaction-test--help)))
|
||||
(should (equal
|
||||
(plist-get
|
||||
(ebox-playground-flex-resize-evaluator--effective-face
|
||||
(ebox-playground-interaction-test--property "ADD 5" 'mouse-face))
|
||||
:background)
|
||||
"#F6D6AB"))
|
||||
(should (ebox-buffer-update-report buffer))))))
|
||||
|
||||
(provide 'ebox-playground-interaction-tests)
|
||||
;;; ebox-playground-interaction-tests.el ends here
|
||||
307
tests/ebox-playground-layer-tests.el
Normal file
307
tests/ebox-playground-layer-tests.el
Normal file
@ -0,0 +1,307 @@
|
||||
;;; ebox-playground-layer-tests.el --- Layer lab command regressions -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
;; Exercise the actual companion and DSL through the generic file runner.
|
||||
;; Commands come from rendered native keymaps. Every publication is compared
|
||||
;; with an independent render of the committed logical input, including paint
|
||||
;; and native interaction properties after normalizing runtime identities.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'cl-lib)
|
||||
(require 'ebox-playground)
|
||||
(require 'ebox-playground-flex-resize-evaluator
|
||||
(expand-file-name "scripts/ebox-playground-flex-resize-evaluator.el"
|
||||
ebox-playground-directory))
|
||||
|
||||
(defconst ebox-playground-layer-test--file
|
||||
(expand-file-name "examples/layer-reference.ebox" ebox-playground-directory)
|
||||
"Layer lab loaded by this suite through the ordinary file runner.")
|
||||
|
||||
(cl-defmacro ebox-playground-layer-test--with-preview
|
||||
((buffer &optional (width 720)) &rest body)
|
||||
"Run BODY in a fresh BUFFER preview of WIDTH pixels and clean up afterward."
|
||||
(declare (indent 1) (debug ((symbolp &optional form) body)))
|
||||
`(let ((,buffer (generate-new-buffer " *ebox-layer-test*"))
|
||||
(ebox-viewport-width ,width) (ebox-viewport-height 100)
|
||||
(case-fold-search nil)
|
||||
(ebox-runtime-idle-prewarm nil)
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-playground-open-file
|
||||
ebox-playground-layer-test--file (buffer-name ,buffer))
|
||||
(with-current-buffer ,buffer ,@body))
|
||||
(when (buffer-live-p ,buffer)
|
||||
(ebox-playground-close (buffer-name ,buffer))))))
|
||||
|
||||
(defun ebox-playground-layer-test--position (label)
|
||||
"Return the first visible position of LABEL in the current preview."
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(unless (search-forward label nil t)
|
||||
(ert-fail (format "Missing visible layer label: %s" label)))
|
||||
(- (point) (length label))))
|
||||
|
||||
(defun ebox-playground-layer-test--coordinates (label)
|
||||
"Return LABEL's visible horizontal pixel and zero-based row coordinates."
|
||||
(save-excursion
|
||||
(goto-char (ebox-playground-layer-test--position label))
|
||||
(cons (ebox-string-pixel-width
|
||||
(buffer-substring (line-beginning-position) (point)))
|
||||
(1- (line-number-at-pos)))))
|
||||
|
||||
(defun ebox-playground-layer-test--parity ()
|
||||
"Compare native paint and interactions with the committed snapshot render."
|
||||
(let* ((state (ebox--buffer-render-state (current-buffer)))
|
||||
(ebox-viewport-width (plist-get state :viewport-width))
|
||||
(ebox-viewport-height (plist-get state :viewport-height))
|
||||
(snapshot (ebox-surface-buffer-snapshot (current-buffer)))
|
||||
(fresh (ebox-render (plist-get snapshot :input))))
|
||||
(should (ebox-canonical-input-p (plist-get snapshot :input)))
|
||||
(should
|
||||
(equal-including-properties
|
||||
(ebox-playground-scroll-evaluator--visual-text (buffer-string))
|
||||
(ebox-playground-scroll-evaluator--visual-text fresh)))))
|
||||
|
||||
(defun ebox-playground-layer-test--activate (label &optional key)
|
||||
"Invoke the native command at LABEL using KEY, then verify its publication."
|
||||
(let* ((keys (or key (kbd "RET")))
|
||||
(position (ebox-playground-layer-test--position label))
|
||||
(map (get-text-property position 'keymap)))
|
||||
(goto-char position)
|
||||
(should (keymapp map))
|
||||
(let ((command (key-binding keys))
|
||||
(last-input-event (aref (vconcat keys) (1- (length keys)))))
|
||||
(should (commandp command))
|
||||
(should (eq command (lookup-key map keys)))
|
||||
(call-interactively command)
|
||||
(ebox-playground-layer-test--parity)
|
||||
command)))
|
||||
|
||||
(defun ebox-playground-layer-test--click (label)
|
||||
"Dispatch LABEL's native mouse command while another buffer is current."
|
||||
(let* ((buffer (current-buffer))
|
||||
(position (ebox-playground-layer-test--position label))
|
||||
(map (get-text-property position 'keymap)))
|
||||
(save-window-excursion
|
||||
(set-window-buffer (selected-window) buffer)
|
||||
(let* ((event (list 'mouse-1 (list (selected-window) position '(0 . 0) 0)))
|
||||
(last-input-event event)
|
||||
(command (key-binding [mouse-1] nil nil position)))
|
||||
(should (commandp command))
|
||||
(should (eq command (lookup-key map [mouse-1])))
|
||||
(with-temp-buffer
|
||||
(call-interactively command nil (vector event))
|
||||
(should (zerop (buffer-size))))))
|
||||
(ebox-playground-layer-test--parity)))
|
||||
|
||||
(defun ebox-playground-layer-test--nodes (form)
|
||||
"Return DSL nodes in FORM without walking opaque property values."
|
||||
(when (and (consp form) (memq (car form) '(box text row column flex grid)))
|
||||
(let ((tail (cdr form)) (nodes (list form)))
|
||||
(while tail
|
||||
(if (keywordp (car tail))
|
||||
(setq tail (cddr tail))
|
||||
(setq nodes (append nodes (ebox-playground-layer-test--nodes (car tail)))
|
||||
tail (cdr tail))))
|
||||
nodes)))
|
||||
|
||||
(ert-deftest ebox-playground-layer-reference-declares-layers-with-one-font-size ()
|
||||
"Actual layout data contains the taught contracts and no font-size override."
|
||||
(let* ((form (ebox-playground--evaluate-form
|
||||
(ebox-playground--read-file ebox-playground-layer-test--file)
|
||||
ebox-playground-layer-test--file))
|
||||
(nodes (ebox-playground-layer-test--nodes form))
|
||||
(owner (cl-find-if
|
||||
(lambda (node) (equal (plist-get (cdr node) :id) "menu-owner"))
|
||||
nodes))
|
||||
(menu (cl-find-if
|
||||
(lambda (node) (equal (plist-get (cdr node) :id) "root-menu"))
|
||||
(ebox-playground-layer-test--nodes owner))))
|
||||
(should (ebox-canonical-input-p (ebox-build form)))
|
||||
(should (eq (plist-get (cdr owner) :overflow) 'hidden))
|
||||
(should (equal (plist-get (cdr owner) :height) '(lh 1)))
|
||||
(should (eq (plist-get (cdr menu) :layer) 'root))
|
||||
(should (equal (plist-get (cdr menu) :anchor) "menu-trigger"))
|
||||
(dolist (position '(relative absolute))
|
||||
(should (cl-some (lambda (node)
|
||||
(eq (plist-get (cdr node) :position) position)) nodes)))
|
||||
(dolist (node nodes)
|
||||
(should-not (plist-member (cdr node) :isolation))
|
||||
(should-not (plist-member (cdr node) :font-size)))))
|
||||
|
||||
(ert-deftest ebox-playground-layer-covered-updates-reveal-latest-native-surface ()
|
||||
"Covered text remains retained; revealing it restores current native help."
|
||||
(ebox-playground-layer-test--with-preview (buffer)
|
||||
(should-not (string-match-p "LOWER REV" (buffer-string)))
|
||||
(should (ebox-region-resolve buffer "lower-note"))
|
||||
(ebox-playground-layer-test--activate "UPDATE COVERED NOTE")
|
||||
(ebox-playground-layer-test--activate "UPDATE COVERED NOTE" (kbd "SPC"))
|
||||
(should-not (string-match-p "LOWER REV" (buffer-string)))
|
||||
(should (string-match-p "lower rev=02" (buffer-string)))
|
||||
(ebox-playground-layer-test--activate "TOGGLE PANEL A")
|
||||
(let* ((position (ebox-playground-layer-test--position "LOWER REV 02"))
|
||||
(help (get-text-property position 'help-echo)))
|
||||
(should (eq (get-text-property position 'pointer) 'hand))
|
||||
(should (get-text-property position 'mouse-face))
|
||||
(should (functionp help))
|
||||
(should (string-match-p "revision 2; activated 0"
|
||||
(funcall help (selected-window) buffer position))))
|
||||
(ebox-playground-layer-test--activate "LOWER REV 02" [return])
|
||||
(let* ((position (ebox-playground-layer-test--position "LOWER REV 02"))
|
||||
(help (get-text-property position 'help-echo)))
|
||||
(should (string-match-p "revision 2; activated 1"
|
||||
(funcall help (selected-window) buffer position))))
|
||||
(ebox-playground-layer-test--activate "TOGGLE PANEL A")
|
||||
(should-not (string-match-p "LOWER REV" (buffer-string)))
|
||||
(ebox-playground-layer-test--activate "TOGGLE PANEL A")
|
||||
(should (integerp (ebox-playground-layer-test--position "LOWER REV 02")))))
|
||||
|
||||
(ert-deftest ebox-playground-layer-move-and-raise-preserve-flow-and-coverage ()
|
||||
"Moving changes paint coordinates; raising changes only overlap ownership."
|
||||
(ebox-playground-layer-test--with-preview (buffer)
|
||||
(let ((home (ebox-playground-layer-test--coordinates "PANEL A / blue"))
|
||||
(desk-end (ebox-playground-layer-test--coordinates "DESK END")))
|
||||
(should-not (string-match-p "A survives under the amber panel" (buffer-string)))
|
||||
(ebox-playground-layer-test--activate "RAISE / LOWER A")
|
||||
(should (integerp (ebox-playground-layer-test--position
|
||||
"A survives under the amber panel")))
|
||||
(should-not (string-match-p "PANEL B / amber" (buffer-string)))
|
||||
(should (equal home (ebox-playground-layer-test--coordinates "PANEL A / blue")))
|
||||
(ebox-playground-layer-test--activate "RAISE / LOWER A")
|
||||
(should (integerp (ebox-playground-layer-test--position "PANEL B / amber")))
|
||||
(ebox-playground-layer-test--activate "MOVE PANEL A")
|
||||
(let ((moved (ebox-playground-layer-test--coordinates "PANEL A / blue")))
|
||||
(should (> (car moved) (car home)))
|
||||
(should (= (cdr moved) (- (cdr home) 2))))
|
||||
(should (equal desk-end (ebox-playground-layer-test--coordinates "DESK END")))
|
||||
(ebox-playground-layer-test--activate "MOVE PANEL A")
|
||||
(should (equal home (ebox-playground-layer-test--coordinates "PANEL A / blue"))))))
|
||||
|
||||
(ert-deftest ebox-playground-layer-relative-marker-reserves-its-original-row ()
|
||||
"The relative marker moves into row two while row three retains its slot."
|
||||
(ebox-playground-layer-test--with-preview (buffer)
|
||||
(let ((marker (ebox-playground-layer-test--coordinates "RELATIVE / +4ch, +1lh"))
|
||||
(following (ebox-playground-layer-test--coordinates "FLOW ROW 3")))
|
||||
(should (= (1+ (cdr marker)) (cdr following)))
|
||||
(should (> (car marker) (car following)))
|
||||
(save-excursion
|
||||
(goto-char (ebox-playground-layer-test--position "RELATIVE / +4ch, +1lh"))
|
||||
(forward-line -1)
|
||||
(should (string-match-p
|
||||
"\\`[[:space:]]*\\'"
|
||||
(buffer-substring-no-properties
|
||||
(line-beginning-position) (line-end-position)))))
|
||||
(ebox-playground-layer-test--parity))))
|
||||
|
||||
(ert-deftest ebox-playground-layer-root-menu-follows-all-four-anchor-placements ()
|
||||
"The nested menu escapes its one-row owner and follows the moved trigger."
|
||||
(ebox-playground-layer-test--with-preview (buffer)
|
||||
(should-not (string-match-p "ROOT MENU / choose" (buffer-string)))
|
||||
(ebox-playground-layer-test--activate "OPEN / CLOSE ROOT MENU")
|
||||
(let* ((trigger (ebox-playground-layer-test--coordinates "OPEN / CLOSE ROOT MENU"))
|
||||
(bottom-start (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||
(should (= (cdr bottom-start) (1+ (cdr trigger))))
|
||||
(should (= (car bottom-start) (car trigger)))
|
||||
(ebox-playground-layer-test--activate "CYCLE PLACEMENT")
|
||||
(let ((bottom-end (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||
(should (= (cdr bottom-end) (cdr bottom-start)))
|
||||
(should (> (car bottom-end) (car bottom-start)))
|
||||
(ebox-playground-layer-test--activate "CYCLE PLACEMENT")
|
||||
(let ((top-start (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||
(should (= (cdr top-start) (- (cdr trigger) 3)))
|
||||
(should (= (car top-start) (car bottom-start))))
|
||||
(ebox-playground-layer-test--activate "CYCLE PLACEMENT")
|
||||
(let ((top-end (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||
(should (= (cdr top-end) (- (cdr trigger) 3)))
|
||||
(should (= (car top-end) (car bottom-end))))
|
||||
(ebox-playground-layer-test--activate "CYCLE PLACEMENT"))
|
||||
(should (equal bottom-start (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||
(ebox-playground-layer-test--activate "MOVE ANCHOR")
|
||||
(let ((moved-trigger (ebox-playground-layer-test--coordinates "OPEN / CLOSE ROOT MENU"))
|
||||
(moved-menu (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||
(should (> (car moved-trigger) (car trigger)))
|
||||
(should (= (- (car moved-menu) (car bottom-start))
|
||||
(- (car moved-trigger) (car trigger))))))
|
||||
(let ((position (ebox-playground-layer-test--position "CHOOSE SAGE")))
|
||||
(dolist (property '(keymap mouse-face help-echo pointer))
|
||||
(should (get-text-property position property))))
|
||||
(ebox-playground-layer-test--click "CHOOSE SAGE")
|
||||
(should-not (string-match-p "ROOT MENU / choose" (buffer-string)))
|
||||
(should (integerp (ebox-playground-layer-test--position
|
||||
"Selected sage from the anchored menu.")))
|
||||
(ebox-playground-layer-test--activate "OPEN / CLOSE ROOT MENU")
|
||||
(ebox-playground-layer-test--activate "CHOOSE AMBER" (kbd "SPC"))
|
||||
(should (integerp (ebox-playground-layer-test--position
|
||||
"Selected amber from the anchored menu.")))))
|
||||
|
||||
(ert-deftest ebox-playground-layer-previews-retain-independent-state-at-two-widths ()
|
||||
"Separate previews keep independent notes and menus, with bounded rendering."
|
||||
(ebox-playground-layer-test--with-preview (first)
|
||||
(ebox-playground-layer-test--activate "UPDATE COVERED NOTE")
|
||||
(ebox-playground-layer-test--with-preview (second 1000)
|
||||
(should (string-match-p "lower rev=00" (buffer-string)))
|
||||
(ebox-playground-layer-test--activate "OPEN / CLOSE ROOT MENU")
|
||||
(with-current-buffer first
|
||||
(should (string-match-p "lower rev=01" (buffer-string)))
|
||||
(should-not (string-match-p "ROOT MENU / choose" (buffer-string)))
|
||||
(ebox-playground-layer-test--parity))
|
||||
(dolist (line (ebox-string-lines (buffer-string)))
|
||||
(should (<= (ebox-string-pixel-width line) 1000)))
|
||||
(ebox-playground-layer-test--parity))))
|
||||
|
||||
(ert-deftest ebox-playground-layer-orbit-expands-without-moving-following-content ()
|
||||
"The spatial desktop moves its cards while keeping the next section fixed."
|
||||
(ebox-playground-layer-test--with-preview (buffer)
|
||||
(let ((home (ebox-playground-layer-test--coordinates "SIGNAL / NOW PLAYING"))
|
||||
(following (ebox-playground-layer-test--coordinates "01 / LAYERED DESK")))
|
||||
(ebox-playground-layer-test--activate "EXPLODE STACK")
|
||||
(let ((expanded (ebox-playground-layer-test--coordinates "SIGNAL / NOW PLAYING")))
|
||||
(should (> (car expanded) (car home)))
|
||||
(should (= (cdr expanded) (+ (cdr home) 4))))
|
||||
(should (equal following
|
||||
(ebox-playground-layer-test--coordinates "01 / LAYERED DESK")))
|
||||
(should (string-match-p "EXPANDED" (buffer-string)))
|
||||
(ebox-playground-layer-test--activate "EXPLODE STACK" (kbd "SPC"))
|
||||
(should (equal home
|
||||
(ebox-playground-layer-test--coordinates "SIGNAL / NOW PLAYING")))
|
||||
(should (string-match-p "STACKED" (buffer-string))))))
|
||||
|
||||
(ert-deftest ebox-playground-layer-orbit-focus-and-palette-are-reversible ()
|
||||
"Native commands update actual card order and paint, independently per preview."
|
||||
(ebox-playground-layer-test--with-preview (first)
|
||||
(cl-labels ((property (id key)
|
||||
(let* ((handle (ebox-region-resolve (current-buffer) id))
|
||||
(region-id (cdr (ebox-selector--region-target handle))))
|
||||
(ebox-get (ebox--buffer-region-render-owner-node
|
||||
(current-buffer) region-id) key))))
|
||||
(dolist (card '(("orbit-violet" . "STUDIO")
|
||||
("orbit-lime" . "NEXT")
|
||||
("orbit-signal" . "SIGNAL")))
|
||||
(ebox-playground-layer-test--activate "CYCLE FOCUS")
|
||||
(should (= (property (car card) :z-index) 6))
|
||||
(should (string-match-p (concat (cdr card) " IN FRONT") (buffer-string))))
|
||||
(ebox-playground-layer-test--click "SWITCH MOOD")
|
||||
(should (equal (property "orbit-signal" :bgcolor) "#FFD49B"))
|
||||
(should (string-match-p "SOLAR PALETTE" (buffer-string)))
|
||||
(ebox-playground-layer-test--with-preview (second 1000)
|
||||
(should (string-match-p "LUNAR PALETTE" (buffer-string)))
|
||||
(should (equal (property "orbit-signal" :bgcolor) "#B9E9F2")))
|
||||
(ebox-playground-layer-test--activate "SWITCH MOOD")
|
||||
(should (equal (property "orbit-signal" :bgcolor) "#B9E9F2"))
|
||||
(should (string-match-p "LUNAR PALETTE" (buffer-string))))))
|
||||
|
||||
(ert-deftest ebox-playground-layer-orbit-remains-bounded-at-desktop-widths ()
|
||||
"The actual composition and toolbar remain usable across desktop widths."
|
||||
(dolist (width '(80 104 140))
|
||||
(ebox-playground-layer-test--with-preview (buffer width)
|
||||
(should (integerp (ebox-playground-layer-test--position "STUDIO / 01")))
|
||||
(should (integerp (ebox-playground-layer-test--position "SIGNAL / NOW PLAYING")))
|
||||
(ebox-playground-layer-test--activate "EXPLODE STACK")
|
||||
(dolist (line (ebox-string-lines (buffer-string)))
|
||||
(should (<= (ebox-string-pixel-width line) width))))))
|
||||
|
||||
(provide 'ebox-playground-layer-tests)
|
||||
;;; ebox-playground-layer-tests.el ends here
|
||||
377
tests/ebox-playground-size-tests.el
Normal file
377
tests/ebox-playground-size-tests.el
Normal file
@ -0,0 +1,377 @@
|
||||
;;; ebox-playground-size-tests.el --- Size reference contracts -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
;; Keep the Size reference executable and verify declarations, arithmetic,
|
||||
;; and viewport updates instead of treating descriptive labels as coverage.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'cl-lib)
|
||||
(require 'ebox-playground)
|
||||
(require 'ebox-size)
|
||||
(require 'ebox-playground-flex-resize-evaluator
|
||||
(expand-file-name "scripts/ebox-playground-flex-resize-evaluator.el"
|
||||
ebox-playground-directory))
|
||||
|
||||
(defconst ebox-playground-size-test--file
|
||||
(expand-file-name "examples/size-reference.ebox" ebox-playground-directory)
|
||||
"Size reference checked by this suite.")
|
||||
|
||||
(defun ebox-playground-size-test--form ()
|
||||
"Read the actual Size example through the ordinary file boundary."
|
||||
(ebox-playground--evaluate-form
|
||||
(ebox-playground--read-file ebox-playground-size-test--file)))
|
||||
|
||||
(defun ebox-playground-size-test--lists (form)
|
||||
"Return the nonempty lists recursively contained in FORM."
|
||||
(when (consp form)
|
||||
(cons form (cl-mapcan #'ebox-playground-size-test--lists form))))
|
||||
|
||||
(defun ebox-playground-size-test--nodes (form)
|
||||
"Return actual DSL nodes in FORM, excluding size expressions."
|
||||
(cl-remove-if-not
|
||||
(lambda (item) (memq (car item) '(box text row column flex grid)))
|
||||
(ebox-playground-size-test--lists form)))
|
||||
|
||||
(defun ebox-playground-size-test--node (id)
|
||||
"Return the actual demonstration node named ID."
|
||||
(or (cl-find-if
|
||||
(lambda (node) (equal (format "%s" (plist-get (cdr node) :id)) id))
|
||||
(ebox-playground-size-test--nodes (ebox-playground-size-test--form)))
|
||||
(ert-fail (format "Missing Size demonstration: %s" id))))
|
||||
|
||||
(defun ebox-playground-size-test--card (id)
|
||||
"Return the authored outer card containing demonstration ID."
|
||||
(or (cl-find-if
|
||||
(lambda (node)
|
||||
(and (eq (car node) 'column)
|
||||
(plist-get (cdr node) :border)
|
||||
(cl-some (lambda (child)
|
||||
(equal (plist-get (cdr child) :id) id))
|
||||
(ebox-playground-size-test--nodes node))))
|
||||
(ebox-playground-size-test--nodes (ebox-playground-size-test--form)))
|
||||
(ert-fail (format "Missing Size card: %s" id))))
|
||||
|
||||
(defun ebox-playground-size-test--color-widths (line color)
|
||||
"Return contiguous visible background COLOR widths in rendered LINE."
|
||||
(let ((line (ebox-playground-flex-resize-evaluator--visual-text line))
|
||||
(start 0) (run 0) widths)
|
||||
(while (< start (length line))
|
||||
(let ((end (next-single-property-change
|
||||
start 'face line (length line))))
|
||||
(if (equal (plist-get (get-text-property start 'face line) :background)
|
||||
color)
|
||||
(setq run (+ run (ebox-string-pixel-width
|
||||
(substring line start end))))
|
||||
(when (> run 0) (push run widths))
|
||||
(setq run 0))
|
||||
(setq start end)))
|
||||
(when (> run 0) (push run widths))
|
||||
(nreverse widths)))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-covers-values-in-real-declarations ()
|
||||
"Every advertised unit, function, and keyword must occur as executable data."
|
||||
(let* ((form (ebox-playground-size-test--form))
|
||||
(nodes (ebox-playground-size-test--nodes form))
|
||||
(lists (ebox-playground-size-test--lists form)))
|
||||
(dolist (unit '(px % vw vh ch lh))
|
||||
(should (cl-some (lambda (item)
|
||||
(and (eq (car item) unit)
|
||||
(= (length item) 2)
|
||||
(numberp (cadr item))))
|
||||
lists)))
|
||||
(dolist (function '(calc min max clamp + - * /))
|
||||
(should (cl-some (lambda (item) (eq (car item) function)) lists)))
|
||||
(should (cl-some (lambda (item)
|
||||
(and (memq (car item) '(px % vw vh ch lh))
|
||||
(floatp (cadr item))))
|
||||
lists))
|
||||
(dolist (property '(:width :height :min-width :min-height
|
||||
:max-width :max-height))
|
||||
(dolist (value (append
|
||||
(if (memq property '(:max-width :max-height))
|
||||
'(none) '(auto))
|
||||
'(min-content max-content fit-content stretch)))
|
||||
(ert-info ((format "Missing actual %S %S demonstration" property value))
|
||||
(should (cl-some (lambda (node)
|
||||
(and (plist-member (cdr node) property)
|
||||
(eq (plist-get (cdr node) property) value)))
|
||||
nodes)))))
|
||||
(should (ebox-canonical-input-p (ebox-build form)))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-renders-full-document-at-three-widths ()
|
||||
"The complete reference remains readable beyond its initially visible page."
|
||||
(dolist (width '(320 720 1000))
|
||||
(let* ((text (ebox-playground-flex-resize-evaluator--example-text
|
||||
ebox-playground-size-test--file width t))
|
||||
(lines (ebox-string-lines text)))
|
||||
(should (string-match-p "Ebox Size Reference" text))
|
||||
(should (> (length lines) 30))
|
||||
(dolist (line lines)
|
||||
(should (<= (ebox-string-pixel-width line) width))))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-expressions-have-known-results ()
|
||||
"Check the example's actual expressions against independent reference values."
|
||||
(let ((context '(:percent-base 220 :viewport-width 800 :viewport-height 600
|
||||
:ch 8 :lh 20)))
|
||||
(dolist (case '(("size-px" :width 180)
|
||||
("size-percent" :width 110)
|
||||
("size-vw" :width 160)
|
||||
("size-ch" :width 96)
|
||||
("size-lh" :height 40)
|
||||
("size-vh" :height 60)
|
||||
("size-calc" :width 196)
|
||||
("size-min" :width 100)
|
||||
("size-max" :width 110)
|
||||
("size-arithmetic" :width 60)
|
||||
("size-nested" :width 160)))
|
||||
(let* ((node (ebox-playground-size-test--node (car case)))
|
||||
(expression (plist-get (cdr node) (cadr case))))
|
||||
(ert-info ((format "Size demo %s: %S" (car case) expression))
|
||||
(should (= (ebox-size-resolve expression context) (nth 2 case))))))
|
||||
(should (= (ebox-size-resolve
|
||||
(plist-get (cdr (ebox-playground-size-test--node
|
||||
"size-percent-parent")) :width)
|
||||
context)
|
||||
222))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-labels-keep-a-common-font ()
|
||||
"Only the title is bold and only measured font samples change text size."
|
||||
(let ((nodes (ebox-playground-size-test--nodes
|
||||
(ebox-playground-size-test--form)))
|
||||
font-samples bold-nodes)
|
||||
(dolist (node nodes)
|
||||
(when (plist-member (cdr node) :font-size)
|
||||
(push (plist-get (cdr node) :id) font-samples))
|
||||
(when (eq (plist-get (cdr node) :font-weight) 'bold)
|
||||
(push node bold-nodes))
|
||||
(should-not (plist-member (cdr node) :padding-block)))
|
||||
(should (equal (sort font-samples #'string-lessp)
|
||||
'("size-font-large" "size-font-small")))
|
||||
(should (= (length bold-nodes) 1))
|
||||
(should (equal (car (last (car bold-nodes))) "Ebox Size Reference"))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-card-and-target-widths-match-copy ()
|
||||
"Actual borders and colored targets use the stated 220/112 px references."
|
||||
(dolist (case '(("size-percent" "#D97757" 222 110)
|
||||
("size-calc" "#AFA3D1" 222 196)
|
||||
("size-max" "#AFA3D1" 222 110)
|
||||
("size-width-auto" "#7DB8CE" 114 112)
|
||||
("size-width-stretch" "#7DB8CE" 114 112)
|
||||
("size-max-width-none" "#7DB8CE" 114 112)))
|
||||
(let* ((text (ebox-render
|
||||
(ebox-build (ebox-playground-size-test--card (car case)))))
|
||||
(lines (ebox-string-lines text))
|
||||
(target-widths
|
||||
(cl-mapcan (lambda (line)
|
||||
(ebox-playground-size-test--color-widths
|
||||
line (nth 1 case)))
|
||||
lines)))
|
||||
(ert-info ((format "Card %s" (car case)))
|
||||
(should (cl-every (lambda (line)
|
||||
(= (ebox-string-pixel-width line) (nth 2 case)))
|
||||
lines))
|
||||
(should (equal target-widths (make-list 2 (nth 3 case))))))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-clipping-and-box-model-match-copy ()
|
||||
"Descriptions must agree with real clipping and visible box-model content."
|
||||
(dolist (id '("size-fractional-height" "size-whole-height"))
|
||||
(let ((text (ebox-render
|
||||
(ebox-build (ebox-playground-size-test--node id)))))
|
||||
(should (= (ebox-string-height text) 2))
|
||||
(should (string-match-p "line one" text))
|
||||
(should (string-match-p "line two" text))
|
||||
(should-not (string-match-p "line three" text))))
|
||||
(let ((text (ebox-render
|
||||
(ebox-build (ebox-playground-size-test--node "size-box-model")))))
|
||||
(should (string-match-p "padding" text))
|
||||
(should (string-match-p "inside the border" text)))
|
||||
(let ((width (plist-get
|
||||
(cdr (ebox-playground-size-test--node "size-fractional-width"))
|
||||
:width)))
|
||||
(should (= (ebox-size-resolve width '(:viewport-width 853)) 102.36))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-height-equivalence-is-intentional ()
|
||||
"Content height keywords agree, while lower bounds and stretch differ."
|
||||
(dolist (property '("height" "min-height" "max-height"))
|
||||
(dolist (keyword (cons (if (equal property "max-height") "none" "auto")
|
||||
'("min-content" "max-content" "fit-content" "stretch")))
|
||||
(let* ((id (format "size-%s-%s" property keyword))
|
||||
(text (ebox-render
|
||||
(ebox-build (ebox-playground-size-test--card id))))
|
||||
(colored-lines
|
||||
(cl-count-if
|
||||
(lambda (line)
|
||||
(ebox-playground-size-test--color-widths line "#D9C46D"))
|
||||
(ebox-string-lines text)))
|
||||
(expected (cond ((member keyword '("stretch" "none")) 5)
|
||||
((and (equal property "min-height")
|
||||
(equal keyword "auto")) 1)
|
||||
(t 3))))
|
||||
(ert-info ((format "%s visible height" id))
|
||||
(should (= colored-lines expected)))))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-long-content-shows-three-widths ()
|
||||
"The same constrained text visibly separates all three intrinsic widths."
|
||||
(let ((widths
|
||||
(mapcar
|
||||
(lambda (keyword)
|
||||
(let* ((id (concat "size-width-long-" keyword))
|
||||
(text (ebox-render
|
||||
(ebox-build (ebox-playground-size-test--card id))))
|
||||
(runs (cl-mapcan
|
||||
(lambda (line)
|
||||
(ebox-playground-size-test--color-widths
|
||||
line "#7DB8CE"))
|
||||
(ebox-string-lines text))))
|
||||
;; Width, minimum, and maximum samples share one card.
|
||||
(should (= (length runs) 12))
|
||||
(should (cl-every (lambda (width) (= width (car runs))) runs))
|
||||
;; All words survive all three constraints without being split
|
||||
;; using the obsolete preferred width or clipped from the box.
|
||||
(dolist (word '("red" "ink" "blue"))
|
||||
(should (= (length (split-string text word)) 4)))
|
||||
(car runs)))
|
||||
'("min-content" "fit-content" "max-content"))))
|
||||
(should (< (nth 0 widths) (nth 1 widths) (nth 2 widths)))
|
||||
(should (= (nth 0 widths) (ebox-string-pixel-width "blue")))
|
||||
(should (= (nth 1 widths) (* 8 (ebox-string-pixel-width "0"))))
|
||||
(should (= (nth 2 widths) (ebox-string-pixel-width "red ink on blue")))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-row-distinguishes-auto-and-stretch ()
|
||||
"Row context makes automatic content sizing visibly shorter than stretch."
|
||||
(let ((widths
|
||||
(mapcar
|
||||
(lambda (keyword)
|
||||
(let* ((text (ebox-render
|
||||
(ebox-build
|
||||
(ebox-playground-size-test--card
|
||||
(concat "size-width-row-" keyword)))))
|
||||
(runs (cl-mapcan
|
||||
(lambda (line)
|
||||
(ebox-playground-size-test--color-widths
|
||||
line "#7DB8CE"))
|
||||
(ebox-string-lines text))))
|
||||
(should runs)
|
||||
(should (cl-every (lambda (width) (= width (car runs))) runs))
|
||||
(car runs)))
|
||||
'("auto" "stretch"))))
|
||||
(should (< (car widths) (cadr widths)))
|
||||
(should (= (car widths) (ebox-string-pixel-width "red ink")))
|
||||
(should (= (cadr widths) 112))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-functions-show-boundary-transitions ()
|
||||
"Actual rendered bars distinguish capping, flooring, and both constraints."
|
||||
(dolist (case '(("min" 24 96 144) ("max" 48 96 192)
|
||||
("clamp" 48 96 144)))
|
||||
(cl-mapc
|
||||
(lambda (regime expected)
|
||||
(let* ((id (format "size-function-%s-%s" (car case) regime))
|
||||
(text (ebox-render
|
||||
(ebox-build (ebox-playground-size-test--node id)))))
|
||||
(ert-info ((format "%s rendered extent" id))
|
||||
(should (= (apply #'max (mapcar #'ebox-string-pixel-width
|
||||
(ebox-string-lines text)))
|
||||
expected)))))
|
||||
'("low" "mid" "high") (cdr case))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-units-reach-painted-sizes ()
|
||||
"Verify units and percentages in actual parent and viewport contexts."
|
||||
(let ((ebox-viewport-width 800) (ebox-viewport-height 40))
|
||||
(dolist (case '(("size-percent" "#D97757" 110 2)
|
||||
("size-vw" "#D97757" 160 2)
|
||||
("size-ch" "#D97757" 12 2)
|
||||
("size-lh" "#D97757" 220 2)
|
||||
("size-vh" "#D97757" 220 4)
|
||||
("size-percent-height" "#8FAF95" 220 3)))
|
||||
(let* ((text (ebox-render
|
||||
(ebox-build (ebox-playground-size-test--card (car case)))))
|
||||
(runs (cl-mapcan
|
||||
(lambda (line)
|
||||
(ebox-playground-size-test--color-widths line (nth 1 case)))
|
||||
(ebox-string-lines text)))
|
||||
(width (if (equal (car case) "size-ch")
|
||||
(min 220 (* 12 (ebox-string-pixel-width "0")))
|
||||
(nth 2 case))))
|
||||
(ert-info ((format "%s painted size" (car case)))
|
||||
(should (equal runs (make-list (nth 3 case) width))))))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-flex-growth-has-visible-shares ()
|
||||
"Equal bases expose spare-space growth without font-dependent wrapping."
|
||||
(let* ((ebox-viewport-width 220)
|
||||
(text (ebox-render
|
||||
(ebox-build (ebox-playground-size-test--node "size-flex-grow"))))
|
||||
(lines (ebox-string-lines text)))
|
||||
(should (= (length lines) 2))
|
||||
(dolist (line lines)
|
||||
(should (= (ebox-string-pixel-width line) 220))
|
||||
(should (equal (ebox-playground-size-test--color-widths line "#9FD5CB")
|
||||
'(80)))
|
||||
(should (equal (ebox-playground-size-test--color-widths line "#71BDB0")
|
||||
'(130))))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-grid-paint-shows-complete-tracks ()
|
||||
"Cell backgrounds expose both lines of each track and leave the gap clear."
|
||||
(let* ((ebox-viewport-width 220)
|
||||
(text (ebox-render
|
||||
(ebox-build (ebox-playground-size-test--node "size-grid"))))
|
||||
(lines (ebox-string-lines text))
|
||||
(ch (ebox-string-pixel-width "0")))
|
||||
(should (= (length lines) 5))
|
||||
(cl-loop for line in lines for index from 0 do
|
||||
(should (= (ebox-string-pixel-width line) 220))
|
||||
(dolist (case `(("#9FD5CB" . ,(* 8 ch))
|
||||
("#71BDB0" . ,(- 220 (* 9 ch)))))
|
||||
(should (equal
|
||||
(ebox-playground-size-test--color-widths line (car case))
|
||||
(unless (= index 2) (list (cdr case)))))))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-clamp-reaches-all-three-regimes ()
|
||||
"The responsive clamp demo must exercise its minimum, fluid value, and cap."
|
||||
(let ((expression (plist-get
|
||||
(cdr (ebox-playground-size-test--node "size-clamp"))
|
||||
:width)))
|
||||
(dolist (case '((300 . 48) (800 . 96) (2000 . 200)))
|
||||
(should (= (ebox-size-resolve expression
|
||||
(list :viewport-width (car case)))
|
||||
(cdr case))))))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-fixed-demos-render-their-extents ()
|
||||
"Fixed pixel and line demonstrations must reach the public renderer intact."
|
||||
(dolist (case '(("size-px" . 180) ("size-arithmetic" . 60)))
|
||||
(let ((text (ebox-render
|
||||
(ebox-build (ebox-playground-size-test--node (car case))))))
|
||||
(should (= (apply #'max (mapcar #'ebox-string-pixel-width
|
||||
(ebox-string-lines text)))
|
||||
(cdr case)))))
|
||||
(should (= (ebox-string-height
|
||||
(ebox-render (ebox-build
|
||||
(ebox-playground-size-test--node "size-lh"))))
|
||||
2)))
|
||||
|
||||
(ert-deftest ebox-playground-size-reference-retained-resize-matches-fresh-render ()
|
||||
"Changing both viewport axes must update responsive sizes without stale output."
|
||||
(let ((buffer (generate-new-buffer " *ebox-size-reference-test*"))
|
||||
(ebox-runtime-idle-prewarm nil)
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||
(unwind-protect
|
||||
(let ((ebox-viewport-width 720) (ebox-viewport-height 36))
|
||||
(ebox-render-to-buffer
|
||||
buffer (ebox-playground-view ebox-playground-size-test--file 720))
|
||||
(dolist (size '((1000 48) (480 24) (720 36)))
|
||||
(let* ((ebox-viewport-width (car size))
|
||||
(ebox-viewport-height (cadr size))
|
||||
(fresh (ebox-render
|
||||
(ebox-playground-view
|
||||
ebox-playground-size-test--file (car size)))))
|
||||
(ebox-rerender-buffer-with-context buffer (car size) (cadr size))
|
||||
(should
|
||||
(equal-including-properties
|
||||
(ebox-playground-flex-resize-evaluator--visual-text fresh)
|
||||
(ebox-playground-flex-resize-evaluator--visual-text
|
||||
(with-current-buffer buffer (buffer-string))))))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(provide 'ebox-playground-size-tests)
|
||||
;;; ebox-playground-size-tests.el ends here
|
||||
@ -3,20 +3,26 @@
|
||||
(require 'ert)
|
||||
(require 'cl-lib)
|
||||
(require 'ebox-playground)
|
||||
(require 'ebox-playground-flex-resize-evaluator
|
||||
(expand-file-name "scripts/ebox-playground-flex-resize-evaluator.el"
|
||||
ebox-playground-directory))
|
||||
|
||||
(defconst ebox-playground-test--fixtures
|
||||
'("basic.ebox" "comprehensive.ebox" "flex-reference.ebox"
|
||||
"responsive-reference.ebox" "grid-reference.ebox")
|
||||
'("flex-reference.ebox" "grid-reference.ebox" "size-reference.ebox")
|
||||
"Migrated and maintained `.ebox' fixtures in the playground package.")
|
||||
|
||||
(defun ebox-playground-test--dynamic-width ()
|
||||
"Return a list-valued width from executable DSL property code."
|
||||
'(240))
|
||||
"Return a list-valued width from an ordinary Elisp expression."
|
||||
'(px 240))
|
||||
|
||||
(defun ebox-playground-test--dynamic-color ()
|
||||
"Return a color from executable DSL property code."
|
||||
"Return a color from an ordinary Elisp expression."
|
||||
"#123456")
|
||||
|
||||
(defun ebox-playground-test--read-layout (file)
|
||||
"Read and evaluate the layout expression in FILE."
|
||||
(ebox-playground--evaluate-form (ebox-playground--read-file file) file))
|
||||
|
||||
(defun ebox-playground-test--boxes (form)
|
||||
"Return every nested BOX form in FORM."
|
||||
(when (consp form)
|
||||
@ -40,6 +46,12 @@
|
||||
(setq items (cddr items)))
|
||||
items))
|
||||
|
||||
(defun ebox-playground-test--separator-form-p (form)
|
||||
"Return non-nil for an empty gallery separator FORM, regardless of defaults."
|
||||
(and (eq (car-safe form) 'box)
|
||||
(null (ebox-playground-test--form-children form))
|
||||
(equal (plist-get (cdr form) :bgcolor) "#FAF7F0")))
|
||||
|
||||
(defun ebox-playground-test--line-widths (text)
|
||||
"Return rendered pixel widths for every line in TEXT."
|
||||
(mapcar #'ebox-string-pixel-width
|
||||
@ -49,94 +61,209 @@
|
||||
"Return the widest rendered pixel line in TEXT."
|
||||
(apply #'max (ebox-playground-test--line-widths text)))
|
||||
|
||||
(defun ebox-playground-test--absolute-space-width (line)
|
||||
"Return LINE's explicit pixel space width, preserving fractional precision."
|
||||
(pcase (get-text-property 0 'display line)
|
||||
(`(space :width (,width)) (and (numberp width) width))))
|
||||
|
||||
(defconst ebox-playground-test--grid-value-forms
|
||||
'(":grid-template-columns '(auto auto)"
|
||||
":grid-template-columns '((120) (74))"
|
||||
":padding '(0 (2))"
|
||||
":grid-template-columns '((192))"
|
||||
":grid-template-columns '((88) (88))"
|
||||
":grid-template-columns '(1fr 2fr)"
|
||||
":grid-template-columns '((fr 1) (fr 2))"
|
||||
":grid-template-columns '((minmax"
|
||||
":grid-template-columns '((repeat"
|
||||
":grid-template-rows '(auto auto)"
|
||||
":grid-template-rows '(1fr 2fr)"
|
||||
":grid-auto-columns '((64))"
|
||||
":grid-auto-columns 'auto"
|
||||
":grid-auto-columns '1fr"
|
||||
":grid-auto-columns '((minmax"
|
||||
":grid-auto-rows 2"
|
||||
":grid-auto-rows 'auto"
|
||||
":grid-auto-rows '1fr"
|
||||
":grid-auto-rows '((minmax"
|
||||
":grid-auto-flow 'row"
|
||||
":grid-auto-flow 'column"
|
||||
":gap 1"
|
||||
":gap '(1 (14))"
|
||||
":row-gap 2"
|
||||
":column-gap '(28)"
|
||||
":grid-row-gap 2"
|
||||
":grid-column-gap '(28)"
|
||||
'(":grid-template-columns (auto auto)"
|
||||
":grid-template-columns (min-content max-content)"
|
||||
":grid-template-columns ((px 120) (px 74))"
|
||||
":padding-inline (px 2)"
|
||||
":grid-template-columns ((px 192))"
|
||||
":grid-template-columns ((px 88) (px 88))"
|
||||
":grid-template-columns ((fr 1) (fr 2))"
|
||||
":grid-template-columns ((minmax"
|
||||
":grid-template-columns ((repeat"
|
||||
":grid-template-rows (auto auto)"
|
||||
":grid-template-rows ((fr 1) (fr 2))"
|
||||
":grid-auto-columns ((px 64))"
|
||||
":grid-auto-columns auto"
|
||||
":grid-auto-columns (fr 1)"
|
||||
":grid-auto-columns ((minmax"
|
||||
":grid-auto-rows (lh 2)"
|
||||
":grid-auto-rows auto"
|
||||
":grid-auto-rows (fr 1)"
|
||||
":grid-auto-rows ((minmax"
|
||||
":grid-auto-flow row"
|
||||
":grid-auto-flow column"
|
||||
":gap ((lh 1) (ch 1))"
|
||||
":gap ((lh 1) (px 14))"
|
||||
":row-gap (lh 2)"
|
||||
":column-gap (px 28)"
|
||||
":grid-column 2"
|
||||
":grid-row 2"
|
||||
":grid-column '(1 :span 2)"
|
||||
":grid-row '(1 :span 2)"
|
||||
":grid-column '(1 3)"
|
||||
":grid-row '(1 3)"
|
||||
":grid-column (1 :span 2)"
|
||||
":grid-row (1 :span 2)"
|
||||
":grid-column (1 3)"
|
||||
":grid-row (1 3)"
|
||||
":grid-column-span 2"
|
||||
":grid-row-span 2"
|
||||
":justify-items 'start"
|
||||
":justify-items 'center"
|
||||
":justify-items 'end"
|
||||
":justify-items 'stretch"
|
||||
":justify-items 'normal"
|
||||
":align-items 'start"
|
||||
":align-items 'center"
|
||||
":align-items 'end"
|
||||
":align-items 'stretch"
|
||||
":align-items 'normal"
|
||||
":justify-content 'start"
|
||||
":justify-content 'center"
|
||||
":justify-content 'end"
|
||||
":justify-content 'space-between"
|
||||
":justify-content 'space-around"
|
||||
":justify-content 'space-evenly"
|
||||
":justify-content 'stretch"
|
||||
":justify-content 'normal"
|
||||
":align-content 'start"
|
||||
":align-content 'center"
|
||||
":align-content 'end"
|
||||
":align-content 'space-between"
|
||||
":align-content 'space-around"
|
||||
":align-content 'space-evenly"
|
||||
":align-content 'stretch"
|
||||
":align-content 'normal")
|
||||
":justify-items start"
|
||||
":justify-items center"
|
||||
":justify-items end"
|
||||
":justify-items stretch"
|
||||
":align-items start"
|
||||
":align-items center"
|
||||
":align-items end"
|
||||
":align-items stretch"
|
||||
":align-items normal"
|
||||
":justify-content start"
|
||||
":justify-content center"
|
||||
":justify-content end"
|
||||
":justify-content space-between"
|
||||
":justify-content space-around"
|
||||
":justify-content space-evenly"
|
||||
":justify-content stretch"
|
||||
":justify-content normal"
|
||||
":align-content start"
|
||||
":align-content center"
|
||||
":align-content end"
|
||||
":align-content space-between"
|
||||
":align-content space-around"
|
||||
":align-content space-evenly"
|
||||
":align-content stretch"
|
||||
":align-content normal")
|
||||
"Public Grid property/value fragments required in the reference fixture.")
|
||||
|
||||
(ert-deftest ebox-playground-tinted-content-has-explicit-ink ()
|
||||
"Every tinted content box in the public fixtures should set its text color."
|
||||
(dolist (file (append ebox-playground-test--fixtures
|
||||
'("public-layout-gallery.ebox")))
|
||||
(let ((form (ebox-playground--read-file
|
||||
(expand-file-name (concat "examples/" file)
|
||||
ebox-playground-directory))))
|
||||
(dolist (box (ebox-playground-test--boxes form))
|
||||
(let ((properties (cdr box)))
|
||||
(when (and (cl-some #'stringp
|
||||
(ebox-playground-test--form-children box))
|
||||
(memq :bgcolor properties))
|
||||
(should (memq :color properties))))))))
|
||||
(ert-deftest ebox-playground-tinted-content-has-effective-ink ()
|
||||
"Tinted fixture text should have effective ink, including inherited colors."
|
||||
(dolist (file ebox-playground-test--fixtures)
|
||||
(let* ((text (ebox-playground-flex-resize-evaluator--example-text
|
||||
(expand-file-name (concat "examples/" file)
|
||||
ebox-playground-directory)
|
||||
720 t))
|
||||
(normalized (ebox-playground-flex-resize-evaluator--visual-text text)))
|
||||
(dotimes (index (length normalized))
|
||||
(when (string-match-p "[^[:space:]]" (substring normalized index (1+ index)))
|
||||
(let ((face (get-text-property index 'face normalized)))
|
||||
(when (plist-get face :background)
|
||||
(should (stringp (plist-get face :foreground))))))))))
|
||||
|
||||
(ert-deftest ebox-playground-visual-comparison-merges-effective-faces ()
|
||||
"Equivalent face grouping and runtime identities should not create differences."
|
||||
(let ((before (propertize "text" 'face
|
||||
'((:foreground "red" :height 1.5)
|
||||
(:foreground "blue" :background "white" :height 120))
|
||||
'ebox-content-idx 1))
|
||||
(after (propertize "text" 'face
|
||||
'(:height 180 :background "white" :foreground "red")
|
||||
'ebox-content-idx 9)))
|
||||
(should (equal-including-properties
|
||||
(ebox-playground-flex-resize-evaluator--visual-text before)
|
||||
(ebox-playground-flex-resize-evaluator--visual-text after)))))
|
||||
|
||||
(ert-deftest ebox-playground-visual-comparison-preserves-visible-and-interactive-changes ()
|
||||
"Face, geometry, text, and interaction differences must survive normalization."
|
||||
(dolist (values '((face (:foreground "red") (:foreground "blue"))
|
||||
(face (:height 120) (:height 180))
|
||||
(display (space :width (80)) (space :width (81)))
|
||||
(help-echo "first action" "second action")
|
||||
(keymap (keymap (13 . first-command))
|
||||
(keymap (13 . second-command)))
|
||||
(mouse-face (:foreground "red") (:foreground "blue"))))
|
||||
(should-not
|
||||
(equal-including-properties
|
||||
(ebox-playground-flex-resize-evaluator--visual-text
|
||||
(propertize "text" (nth 0 values) (nth 1 values)))
|
||||
(ebox-playground-flex-resize-evaluator--visual-text
|
||||
(propertize "text" (nth 0 values) (nth 2 values))))))
|
||||
(should-not
|
||||
(equal-including-properties
|
||||
(ebox-playground-flex-resize-evaluator--visual-text "text")
|
||||
(ebox-playground-flex-resize-evaluator--visual-text "changed"))))
|
||||
|
||||
(ert-deftest ebox-playground-spacer-auto-width-and-inherited-color-render-equally ()
|
||||
"Explicit spacer height and inherited ink retain geometry and local overrides."
|
||||
(let* ((ebox-viewport-width 720)
|
||||
(explicit
|
||||
'(column :width (px 240)
|
||||
(box :width stretch :height (lh 1) :bgcolor "#FAF7F0")
|
||||
(box :color "#123456" :bgcolor "#FFFFFF" "Inherited ink")
|
||||
(box :color "#654321" :bgcolor "#FFFFFF" "Local ink")))
|
||||
(inherited
|
||||
'(column :width (px 240) :color "#123456"
|
||||
(box :height (lh 1) :bgcolor "#FAF7F0")
|
||||
(box :bgcolor "#FFFFFF" "Inherited ink")
|
||||
(box :color "#654321" :bgcolor "#FFFFFF" "Local ink")))
|
||||
(before (ebox-playground-flex-resize-evaluator--visual-text
|
||||
(ebox-render (ebox-build explicit))))
|
||||
(after (ebox-playground-flex-resize-evaluator--visual-text
|
||||
(ebox-render (ebox-build inherited)))))
|
||||
(should (equal-including-properties before after))
|
||||
(should (equal (plist-get
|
||||
(get-text-property (string-match "Local ink" after) 'face after)
|
||||
:foreground)
|
||||
"#654321"))))
|
||||
|
||||
(ert-deftest ebox-playground-uses-only-public-layout-apis ()
|
||||
"The example should build and render through the public Ebox entry point."
|
||||
(let* ((source (ebox-playground--read-file ebox-playground-default-file))
|
||||
(let* ((source (ebox-playground-test--read-layout
|
||||
ebox-playground-default-file))
|
||||
(node (ebox-playground-view))
|
||||
(rendered (substring-no-properties (ebox-render node))))
|
||||
(should (eq (car source) 'column))
|
||||
(dolist (label '("Ebox Playground" "FIXED + FRACTIONAL"
|
||||
"EXPLICIT PLACEMENT" "PUBLIC COMPOSITION"))
|
||||
(dolist (label '("Ebox CSS Grid Reference" "Container property:"
|
||||
"120 PX"))
|
||||
(should (string-match-p (regexp-quote label) rendered)))))
|
||||
|
||||
(ert-deftest ebox-playground-flex-evaluator-uses-public-observation ()
|
||||
"Keep the Flex resize gate on public Ebox reports without advice."
|
||||
(let* ((source (with-temp-buffer
|
||||
(insert-file-contents
|
||||
(expand-file-name
|
||||
"scripts/ebox-playground-flex-resize-evaluator.el"
|
||||
ebox-playground-directory))
|
||||
(buffer-string)))
|
||||
(resize-source
|
||||
(with-temp-buffer
|
||||
(insert source)
|
||||
(goto-char (point-min))
|
||||
;; The shared file also contains private scroll diagnostics and
|
||||
;; declarative property-name metadata. Guard every resize function
|
||||
;; and helper body, rather than unrelated top-level forms.
|
||||
(let (functions)
|
||||
(condition-case nil
|
||||
(while t
|
||||
(let ((form (read (current-buffer))))
|
||||
(when (and (eq (car-safe form) 'defun)
|
||||
(string-prefix-p
|
||||
"ebox-playground-flex-resize-evaluator-"
|
||||
(symbol-name (cadr form))))
|
||||
(push form functions))))
|
||||
(end-of-file nil))
|
||||
;; Inspect API use, allowing this quoted render metadata name.
|
||||
;; The oracle canonicalizes its ids; it does not call an Ebox
|
||||
;; private function or read mutable runtime state through it.
|
||||
(prin1-to-string
|
||||
(cl-subst '(quote hover-owner-metadata)
|
||||
'(quote ebox--hover-style)
|
||||
(nreverse functions) :test #'equal)))))
|
||||
(runner (with-temp-buffer
|
||||
(insert-file-contents
|
||||
(expand-file-name "scripts/flex-resize-evaluator.sh"
|
||||
ebox-playground-directory))
|
||||
(buffer-string)))
|
||||
(makefile (with-temp-buffer
|
||||
(insert-file-contents
|
||||
(expand-file-name "Makefile" ebox-playground-directory))
|
||||
(buffer-string))))
|
||||
(should (string-match-p "ebox-playground-flex-resize-evaluator-run"
|
||||
resize-source))
|
||||
(should (string-match-p "ebox-buffer-set-observer" resize-source))
|
||||
(should (string-match-p "evaluator-warmup-count 5" source))
|
||||
(should (string-match-p "evaluator-sample-count 30" source))
|
||||
(should (string-match-p "per-update p95 budget" source))
|
||||
(should (string-match-p "EBOX_PLAYGROUND_FLEX_FIXTURE" runner))
|
||||
(should (string-match-p "show HEAD:examples/flex-reference.ebox" runner))
|
||||
(should (string-match-p "performance-prepare:" makefile))
|
||||
(should (string-match-p "native-build" makefile))
|
||||
(should-not (string-match-p "-l \"\$root/ebox-playground.el\"" runner))
|
||||
(dolist (private '("ebox--" "advice-add" "advice-remove"
|
||||
"ebox-native-reflow--"))
|
||||
(should-not (string-match-p (regexp-quote private) resize-source)))))
|
||||
|
||||
(ert-deftest ebox-playground-keeps-layout-in-ebox-source ()
|
||||
"Keep concrete gallery content out of the generic Elisp runner."
|
||||
(let ((runner (with-temp-buffer
|
||||
@ -214,6 +341,183 @@
|
||||
1)))))
|
||||
(ebox-playground-close name))))
|
||||
|
||||
(ert-deftest ebox-playground-evaluator-normalizes-hover-owner-identities ()
|
||||
"Fresh ids compare equally without discarding paint or owner partitions."
|
||||
(let* ((normalize #'ebox-playground-flex-resize-evaluator--visual-text)
|
||||
(left (propertize "AB" 'ebox--hover-style
|
||||
'(:owner 22 :face (:background "red")
|
||||
:base (:foreground "white"))))
|
||||
(fresh (propertize "AB" 'ebox--hover-style
|
||||
'(:owner 96 :face (:background "red")
|
||||
:base (:foreground "white")))))
|
||||
(should (equal-including-properties
|
||||
(funcall normalize left) (funcall normalize fresh)))
|
||||
(should (= (plist-get (get-text-property 0 'ebox--hover-style left) :owner) 22))
|
||||
(dolist (declaration '((:owner 96 :face (:background "blue")
|
||||
:base (:foreground "white"))
|
||||
(:owner 96 :face (:background "red")
|
||||
:base (:foreground "black"))
|
||||
(:owner 97 :face (:background "red")
|
||||
:base (:foreground "white"))))
|
||||
(let ((changed (copy-sequence fresh)))
|
||||
(put-text-property 1 2 'ebox--hover-style declaration changed)
|
||||
(should-not (equal-including-properties
|
||||
(funcall normalize left) (funcall normalize changed)))))))
|
||||
|
||||
(ert-deftest ebox-playground-interaction-evaluator-measures-and-cleans ()
|
||||
"Native repeated commands report their work and release temporary state."
|
||||
(let ((file (make-temp-file "ebox-interaction-evaluator-" nil ".ebox"))
|
||||
(buffers (buffer-list))
|
||||
(windows (window-state-get))
|
||||
(functions
|
||||
(mapcar (lambda (symbol) (cons symbol (symbol-function symbol)))
|
||||
'(ebox--render-layout ebox-surface--surface-plan
|
||||
ebox-surface--rendered-fragments ebox-surface--ensure-node-tree))))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(with-temp-file file
|
||||
(prin1
|
||||
'(let ((count 0))
|
||||
(list 'column :width '(px 120)
|
||||
(list 'text :id "count" "00")
|
||||
(list 'text :keymap
|
||||
(ebox-keymap-create
|
||||
:activate
|
||||
(lambda ()
|
||||
(ebox-region-update
|
||||
"count" :content (format "%02d" (cl-incf count)))))
|
||||
"Add")))
|
||||
(current-buffer)))
|
||||
(let ((report (ebox-playground-interaction-evaluator-run
|
||||
file "Add" "RET" 3 120 8)))
|
||||
(should (plist-get report :fresh-parity))
|
||||
(should (string-match-p "03" (plist-get report :text)))
|
||||
(should (= (plist-get report :publications) 3))
|
||||
(dolist (key '(:mean-ms :p95-ms :max-ms :gc-count :gc-ms :cons-cells
|
||||
:layout-calls :surface-plans :fragment-scans
|
||||
:fragment-characters :node-registrations))
|
||||
(should (>= (plist-get report key) 0))))
|
||||
(cl-letf (((symbol-function 'ebox-region-update)
|
||||
(lambda (&rest _) (error "Injected command failure"))))
|
||||
(should-error (ebox-playground-interaction-evaluator-run
|
||||
file "Add" "RET" 3 120 8)))
|
||||
(should-error (ebox-playground-interaction-evaluator-run
|
||||
file "Missing" "RET" 3 120 8))
|
||||
(should (equal buffers (buffer-list)))
|
||||
(should (equal windows (window-state-get)))
|
||||
(dolist (entry functions)
|
||||
(should (eq (cdr entry) (symbol-function (car entry))))))
|
||||
(delete-file file))))
|
||||
|
||||
(ert-deftest ebox-playground-scroll-evaluator-preserves-live-runtime ()
|
||||
"Profiling multiple files preserves existing mounts, windows, and timers."
|
||||
(let ((file (make-temp-file "ebox-scroll-evaluator-" nil ".ebox"))
|
||||
(live (generate-new-buffer " *ebox-scroll-evaluator-live-test*"))
|
||||
(ebox-runtime-idle-prewarm nil)
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil)
|
||||
(ebox-scroll-lazy-idle-prefetch-lines 0)
|
||||
(layout '(column :width (px 120) :height (lh 4) :overflow scroll
|
||||
(box "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11"))))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(with-temp-file file (prin1 (list 'quote layout) (current-buffer)))
|
||||
(ebox-render-to-buffer live (ebox-build layout))
|
||||
(let* ((runtime (ebox--buffer-render-state live))
|
||||
(region-id
|
||||
(plist-get (plist-get runtime :root-node) :region-id))
|
||||
(scroll-state (ebox--scroll-get-state region-id))
|
||||
(contents (with-current-buffer live (buffer-string)))
|
||||
(windows (window-state-get))
|
||||
(buffers (buffer-list))
|
||||
(timer (run-at-time 3600 nil #'ignore))
|
||||
(ebox--deferred-render-gc-timer timer))
|
||||
(unwind-protect
|
||||
(let* ((standard-output (lambda (_character)))
|
||||
;; Exercise the GUI GC lease without displaying a buffer.
|
||||
(noninteractive nil)
|
||||
(reports (ebox-playground-scroll-evaluator-run
|
||||
(list file file) 121 3 8)))
|
||||
(should (= (length reports) 4))
|
||||
(should (equal (mapcar (lambda (report)
|
||||
(plist-get report :position))
|
||||
reports)
|
||||
'(top middle top middle)))
|
||||
(dolist (report reports)
|
||||
(should (plist-get report :fresh-render-parity))
|
||||
(should (= (plist-get report :steps) 3))
|
||||
(dolist (key '(:mean-ms :p95-ms :max-ms :root-renders
|
||||
:node-styles :style-computations))
|
||||
(should (>= (plist-get report key) 0))))
|
||||
(should (eq runtime (ebox--buffer-render-state live)))
|
||||
(should (eq scroll-state (ebox--scroll-get-state region-id)))
|
||||
(should (equal-including-properties
|
||||
contents (with-current-buffer live (buffer-string))))
|
||||
(should (equal windows (window-state-get)))
|
||||
(should (equal buffers (buffer-list)))
|
||||
(should (eq ebox--deferred-render-gc-timer timer))
|
||||
(should (memq timer timer-list)))
|
||||
(cancel-timer timer))))
|
||||
(when (buffer-live-p live) (kill-buffer live))
|
||||
(delete-file file))))
|
||||
|
||||
(ert-deftest ebox-playground-scroll-evaluator-cleans-after-scroll-error ()
|
||||
"A measured scroll failure removes advice and the evaluator's mount."
|
||||
(let ((file (make-temp-file "ebox-scroll-evaluator-error-" nil ".ebox"))
|
||||
(calls 0)
|
||||
(original-scroll (symbol-function 'ebox--scroll-region-by))
|
||||
(functions
|
||||
(mapcar (lambda (symbol) (cons symbol (symbol-function symbol)))
|
||||
'(ebox-surface--render-candidate
|
||||
ebox-surface--apply-node-style
|
||||
ebox-surface--compute-node-style)))
|
||||
(buffers (buffer-list)))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(with-temp-file file
|
||||
(prin1
|
||||
''(column :width (px 120) :height (lh 2) :overflow scroll
|
||||
(box "0\n1\n2\n3\n4\n5\n6"))
|
||||
(current-buffer)))
|
||||
(cl-letf (((symbol-function 'ebox--scroll-region-by)
|
||||
(lambda (&rest arguments)
|
||||
(when (= (cl-incf calls) 3)
|
||||
(error "Injected measured scroll failure"))
|
||||
(apply original-scroll arguments))))
|
||||
(should-error
|
||||
(ebox-playground-scroll-evaluator-run file 121 3 8)))
|
||||
(should (= calls 3))
|
||||
(should (equal buffers (buffer-list)))
|
||||
(dolist (entry functions)
|
||||
(should (eq (cdr entry) (symbol-function (car entry))))))
|
||||
(delete-file file))))
|
||||
|
||||
(ert-deftest ebox-playground-scroll-evaluator-normalizes-only-invisible-spacers ()
|
||||
"The visual oracle tolerates empty padding but retains visible differences."
|
||||
(let* ((text (propertize "A" 'face '(:foreground "#123456")))
|
||||
(normalize #'ebox-playground-scroll-evaluator--visual-text))
|
||||
(should (equal-including-properties
|
||||
(funcall normalize text)
|
||||
(funcall normalize
|
||||
(concat (propertize " " 'display '(space :width (0))
|
||||
'face '(:foreground "#123456"))
|
||||
text))))
|
||||
(dolist (changed
|
||||
(list (concat (propertize " " 'display '(space :width (1))) text)
|
||||
(propertize "A" 'face '(:foreground "#654321"))
|
||||
(propertize "B" 'face '(:foreground "#123456"))))
|
||||
(should-not (equal-including-properties
|
||||
(funcall normalize text)
|
||||
(funcall normalize changed)))))
|
||||
(dolist (carrier
|
||||
(list (propertize " " 'display '(space :width (0)))
|
||||
(propertize " " 'display '(space :width (0) :height 2))
|
||||
(propertize " " 'display '(space :width (0) :ascent 90))
|
||||
(propertize " " 'display '(space :width (0))
|
||||
'face '(:height 2.0))))
|
||||
(should (> (length (ebox-playground-scroll-evaluator--visual-text
|
||||
(concat carrier "\nA")))
|
||||
2))))
|
||||
|
||||
(ert-deftest ebox-playground-effective-viewport-prefers-window-text-width ()
|
||||
"Interactive viewport resolution should prefer the window text width."
|
||||
(let ((ebox-viewport-width nil))
|
||||
@ -243,7 +547,7 @@
|
||||
(lambda (&optional _frame) 8)))
|
||||
(with-temp-buffer
|
||||
(setq buffer-file-name "/tmp/ebox-playground-viewport-render.ebox")
|
||||
(insert "(box :width '(viewport) :height 1 \"viewport\")")
|
||||
(insert "'(box :width (vw 100) :height (lh 1) \"viewport\")")
|
||||
(ebox-dsl-mode)
|
||||
(ebox-dsl-render)
|
||||
(with-current-buffer preview
|
||||
@ -290,7 +594,7 @@
|
||||
(with-temp-buffer
|
||||
(setq source (current-buffer)
|
||||
buffer-file-name "/tmp/ebox-playground-two-window.ebox")
|
||||
(insert "(box :width '(viewport) :height 1 \"two windows\")")
|
||||
(insert "'(box :width (vw 100) :height (lh 1) \"two windows\")")
|
||||
(ebox-dsl-mode)
|
||||
(cl-letf (((symbol-function 'window-body-width)
|
||||
(lambda (&optional _window _pixelwise) 987))
|
||||
@ -312,17 +616,24 @@
|
||||
(kill-buffer preview)))))
|
||||
|
||||
(ert-deftest ebox-playground-preview-resizes-through-ebox-viewport-update ()
|
||||
"A preview resize should publish an incremental Ebox viewport update."
|
||||
"A queued preview resize should publish an incremental viewport update."
|
||||
(let ((preview "*Ebox Preview: /tmp/ebox-playground-viewport-resize.ebox*")
|
||||
(window-width 987))
|
||||
(window-width 987)
|
||||
(ebox--window-size-change-pending-frames nil)
|
||||
(ebox--window-size-change-timer nil)
|
||||
scheduled)
|
||||
(unwind-protect
|
||||
(cl-letf (((symbol-function 'window-body-width)
|
||||
(lambda (&optional _window _pixelwise) window-width))
|
||||
((symbol-function 'frame-char-width)
|
||||
(lambda (&optional _frame) 8)))
|
||||
(lambda (&optional _frame) 8))
|
||||
((symbol-function 'run-at-time)
|
||||
(lambda (_time _repeat function &rest args)
|
||||
(setq scheduled (cons function args))
|
||||
'queued-viewport)))
|
||||
(with-temp-buffer
|
||||
(setq buffer-file-name "/tmp/ebox-playground-viewport-resize.ebox")
|
||||
(insert "(box :width '(viewport) :height 1 \"viewport\")")
|
||||
(insert "'(box :width (vw 100) :height (lh 1) \"viewport\")")
|
||||
(ebox-dsl-mode)
|
||||
(ebox-dsl-render)
|
||||
(setq window-width 1234)
|
||||
@ -331,7 +642,12 @@
|
||||
(should (memq #'ebox--window-size-change
|
||||
window-size-change-functions))
|
||||
(let ((noninteractive nil))
|
||||
(ebox--window-size-change (window-frame window)))))
|
||||
(ebox--window-size-change (window-frame window))))
|
||||
(should (= (ebox-playground-test--max-line-width
|
||||
(buffer-string))
|
||||
(- 987 16))))
|
||||
(should scheduled)
|
||||
(apply (car scheduled) (cdr scheduled))
|
||||
(with-current-buffer preview
|
||||
(should (= (ebox-playground-test--max-line-width
|
||||
(buffer-string))
|
||||
@ -402,7 +718,8 @@
|
||||
(let ((preview " *ebox-playground-preview-settings-test*"))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-playground--render-node-to-buffer preview "preview" 240)
|
||||
(ebox-playground--render-input-to-buffer
|
||||
preview (ebox-build '(box "preview")) 240)
|
||||
(with-current-buffer preview
|
||||
(should truncate-lines)
|
||||
(should-not (assq 'truncation fringe-indicator-alist))
|
||||
@ -434,18 +751,18 @@
|
||||
(when (get-buffer preview)
|
||||
(kill-buffer preview)))))
|
||||
|
||||
(ert-deftest ebox-playground-flex-reference-paints-root-canvas ()
|
||||
"The Flex reference should carry its canvas color into the preview face."
|
||||
(ert-deftest ebox-playground-flex-reference-preserves-buffer-chrome ()
|
||||
"Root canvas paint should preserve the ordinary buffer face and mode line."
|
||||
(let ((preview " *ebox-playground-flex-canvas-test*")
|
||||
(path (expand-file-name "examples/flex-reference.ebox"
|
||||
ebox-playground-directory)))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-playground--render-node-to-buffer
|
||||
(ebox-playground--render-input-to-buffer
|
||||
preview (ebox-playground-view path 720) 720)
|
||||
(with-current-buffer preview
|
||||
(should (equal face-remapping-alist
|
||||
'((default (:background "#FAF7F0")))))
|
||||
(should-not face-remapping-alist)
|
||||
(should (equal mode-line-format (default-value 'mode-line-format)))
|
||||
(let ((display-signature (ebox--current-display-signature))
|
||||
(matching-cache-entry-p nil)
|
||||
(render-cache
|
||||
@ -478,58 +795,94 @@
|
||||
ebox-playground-directory)))
|
||||
(let ((separators
|
||||
(cl-remove-if-not
|
||||
(lambda (form)
|
||||
(and (equal (plist-get (cdr form) :width)
|
||||
'(quote (viewport)))
|
||||
(equal (plist-get (cdr form) :height) 1)
|
||||
(equal (plist-get (cdr form) :bgcolor) "#FAF7F0")))
|
||||
#'ebox-playground-test--separator-form-p
|
||||
(ebox-playground-test--forms-named
|
||||
'box (ebox-playground--read-file path)))))
|
||||
'box (ebox-playground-test--read-layout path)))))
|
||||
(should (= (length separators) 11)))
|
||||
(dolist (viewport '(691 720))
|
||||
(let* ((ebox-viewport-width viewport)
|
||||
(form (ebox-playground-test--read-layout path))
|
||||
;; Pin this canvas contract without changing the author's root.
|
||||
(_ (setcdr form (plist-put (cdr form) :width '(vw 100))))
|
||||
(lines (ebox-string-lines
|
||||
(ebox-render (ebox-playground-view path viewport))))
|
||||
(ebox-render (ebox-build form))))
|
||||
(separator-lines
|
||||
(cl-remove-if-not
|
||||
(lambda (line)
|
||||
(equal (get-text-property 0 'display line)
|
||||
`(space :width (,viewport))))
|
||||
(let ((width (ebox-playground-test--absolute-space-width line)))
|
||||
(and width (= width viewport))))
|
||||
lines)))
|
||||
(should (= (length separator-lines) 11))
|
||||
(dolist (line separator-lines)
|
||||
(should (equal (get-text-property 0 'display line)
|
||||
`(space :width (,viewport))))
|
||||
(should (= (ebox-playground-test--absolute-space-width line)
|
||||
viewport))
|
||||
(should (= (ebox-string-pixel-width line) viewport))
|
||||
(should (equal (get-text-property 0 'face line)
|
||||
'(:background "#FAF7F0"))))))))
|
||||
|
||||
(ert-deftest ebox-playground-public-layout-gallery-fixed-separators-paint-the-canvas ()
|
||||
"The default public gallery fixed separators should keep canvas coverage."
|
||||
(let* ((path (expand-file-name "examples/public-layout-gallery.ebox"
|
||||
ebox-playground-directory))
|
||||
(ert-deftest ebox-playground-default-fixture-separators-paint-the-canvas ()
|
||||
"The default fixture separators should cover the preview canvas."
|
||||
(let* ((ebox-viewport-width 720)
|
||||
(form (ebox-playground-test--read-layout ebox-playground-default-file))
|
||||
(_ (setcdr form (plist-put (cdr form) :width '(vw 100))))
|
||||
(lines (ebox-string-lines
|
||||
(ebox-render (ebox-playground-view path))))
|
||||
(ebox-render (ebox-build form))))
|
||||
(separators 0))
|
||||
(dolist (line lines)
|
||||
(when (equal (get-text-property 0 'display line)
|
||||
'(space :width (720)))
|
||||
(when (let ((width (ebox-playground-test--absolute-space-width line)))
|
||||
(and width (= width 720)))
|
||||
(setq separators (1+ separators))
|
||||
(should (equal (get-text-property 0 'face line)
|
||||
'(:background "#FAF7F0")))))
|
||||
(should (> separators 0))))
|
||||
|
||||
(ert-deftest ebox-playground-separators-follow-a-narrower-root ()
|
||||
"Explicit and auto gallery separators fill a root narrower than the viewport."
|
||||
(dolist (fixture '(("flex-reference.ebox" . 12)
|
||||
("grid-reference.ebox" . 11)))
|
||||
(dolist (omit-width '(nil t))
|
||||
(let* ((ebox-viewport-width 1000)
|
||||
(path (expand-file-name (concat "examples/" (car fixture))
|
||||
ebox-playground-directory))
|
||||
(form (ebox-playground-test--read-layout path))
|
||||
(parent-width 900))
|
||||
;; Inspect the entire gallery independently of the author's height.
|
||||
(setcdr form (plist-put (cdr form) :width '(vw 90)))
|
||||
(setcdr form (plist-put (cdr form) :height 'auto))
|
||||
(cl-remf (cdr form) :overflow)
|
||||
(setcdr form (append '(:overflow visible) (cdr form)))
|
||||
(dolist (box (ebox-playground-test--form-children form))
|
||||
(when (and (eq (car-safe box) 'box)
|
||||
(null (ebox-playground-test--form-children box)))
|
||||
(should (equal (plist-get (cdr box) :height) '(lh 1)))
|
||||
(if omit-width
|
||||
(cl-remf (cdr box) :width)
|
||||
(setcdr box (plist-put (cdr box) :width 'stretch)))))
|
||||
(let* ((lines (ebox-string-lines (ebox-render (ebox-build form))))
|
||||
(separators
|
||||
(cl-remove-if-not
|
||||
(lambda (line)
|
||||
(let ((face (get-text-property 0 'face line)))
|
||||
(and (ebox-playground-test--absolute-space-width line)
|
||||
(or (equal face '(:background "#FAF7F0"))
|
||||
(equal (car-safe face)
|
||||
'(:background "#FAF7F0"))))))
|
||||
lines)))
|
||||
(should (= (length separators) (cdr fixture)))
|
||||
(dolist (line separators)
|
||||
(should (= (ebox-playground-test--absolute-space-width line)
|
||||
parent-width))))))))
|
||||
|
||||
(ert-deftest ebox-playground-grid-reference-covers-public-properties ()
|
||||
"The Grid reference should cover every public property and supported value form."
|
||||
(let ((source
|
||||
(prin1-to-string
|
||||
(ebox-playground--read-file
|
||||
(ebox-playground-test--read-layout
|
||||
(expand-file-name "examples/grid-reference.ebox"
|
||||
ebox-playground-directory)))))
|
||||
(dolist (property '(:grid-template-columns :grid-template-rows
|
||||
:grid-auto-columns :grid-auto-rows :grid-auto-flow
|
||||
:grid-row-gap :grid-column-gap :gap :row-gap
|
||||
:column-gap
|
||||
:gap :row-gap :column-gap
|
||||
:justify-items :align-items :justify-content
|
||||
:align-content :grid-column :grid-row
|
||||
:grid-column-span :grid-row-span))
|
||||
@ -545,19 +898,22 @@
|
||||
(ebox-playground-open name)
|
||||
(should (buffer-live-p (get-buffer name)))
|
||||
(with-current-buffer name
|
||||
(should (string-match-p "Ebox Playground" (buffer-string)))))
|
||||
(should (string-match-p "Ebox CSS Grid Reference"
|
||||
(buffer-string)))))
|
||||
(ebox-playground-close name))))
|
||||
|
||||
(ert-deftest ebox-playground-fits-a-compact-gui-window ()
|
||||
"Keep the gallery wide enough to teach Grid without crossing 800 px frames."
|
||||
(let* ((node (ebox-playground-view))
|
||||
(gallery-width (plist-get (car (plist-get node :children)) :width))
|
||||
(rendered (ebox-render node))
|
||||
"Keep the default gallery inside a compact 720 px preview canvas."
|
||||
(let* ((viewport 720)
|
||||
(ebox-viewport-width viewport)
|
||||
(form (ebox-playground-test--read-layout ebox-playground-default-file))
|
||||
(_ (setcdr form (plist-put (cdr form) :width '(vw 100))))
|
||||
(input (ebox-build form))
|
||||
(rendered (ebox-render input))
|
||||
(widths (mapcar #'ebox-string-pixel-width
|
||||
(ebox-string-lines rendered)))
|
||||
(maximum (apply #'max widths)))
|
||||
(should (= maximum gallery-width))
|
||||
(should (<= gallery-width 760))))
|
||||
(should (= maximum viewport))))
|
||||
|
||||
(ert-deftest ebox-playground-registers-ebox-file-mode ()
|
||||
"Open Ebox DSL files in the package-owned major mode."
|
||||
@ -574,7 +930,7 @@
|
||||
(unwind-protect
|
||||
(with-temp-buffer
|
||||
(setq buffer-file-name "/tmp/ebox-playground-render-test.ebox")
|
||||
(insert "(box :width '(240) \"Rendered from .ebox\")")
|
||||
(insert "'(box :width (px 240) \"Rendered from .ebox\")")
|
||||
(ebox-dsl-mode)
|
||||
(ebox-dsl-render)
|
||||
(should (eq (window-buffer (selected-window)) (current-buffer)))
|
||||
@ -584,23 +940,212 @@
|
||||
(when (get-buffer preview)
|
||||
(kill-buffer preview)))))
|
||||
|
||||
(ert-deftest ebox-playground-evaluates-property-expressions ()
|
||||
"Evaluate executable property values while preserving quoted constants."
|
||||
(let* ((form (ebox-playground--evaluate-form
|
||||
'(box :width (ebox-playground-test--dynamic-width)
|
||||
:color (ebox-playground-test--dynamic-color)
|
||||
:padding '(0 (4))
|
||||
:text-align 'center
|
||||
"Dynamic property values")))
|
||||
(ert-deftest ebox-playground-evaluates-quoted-layout-as-data ()
|
||||
"Quoted layout lists and symbols reach Ebox without extra evaluation."
|
||||
(let* ((expected '(box :width (px 240) :padding ((lh 0) (px 4))
|
||||
:text-align center "Literal values"))
|
||||
(form (ebox-playground--evaluate-form
|
||||
(ebox-playground--read-form
|
||||
"'(box :width (px 240) :padding ((lh 0) (px 4))
|
||||
:text-align center \"Literal values\")")))
|
||||
(node (ebox-build form)))
|
||||
(should (equal (ebox-style-node-specified-value node :width) '(240)))
|
||||
(should (equal form expected))
|
||||
(should (equal (ebox-style-node-specified-value node :width) '(px 240)))
|
||||
(should (equal (ebox-style-node-specified-value node :padding-inline-start)
|
||||
'(px 4)))
|
||||
(should (eq (ebox-style-node-specified-value node :text-align) 'center))))
|
||||
|
||||
(ert-deftest ebox-playground-evaluates-backquoted-property-expressions ()
|
||||
"Use ordinary unquote expressions to calculate property values."
|
||||
(let* ((form (ebox-playground--evaluate-form
|
||||
'(let ((width (ebox-playground-test--dynamic-width))
|
||||
(color (ebox-playground-test--dynamic-color)))
|
||||
`(box :width ,width :color ,color
|
||||
:padding ((lh 0) (px 4)) :text-align center
|
||||
"Dynamic property values"))))
|
||||
(node (ebox-build form)))
|
||||
(should (equal (ebox-style-node-specified-value node :width) '(px 240)))
|
||||
(should (equal (ebox-style-node-specified-value node :color) "#123456"))
|
||||
(should (equal
|
||||
(ebox-style-node-specified-value node :padding-left)
|
||||
'(4)))
|
||||
(ebox-style-node-specified-value node :padding-inline-start)
|
||||
'(px 4)))
|
||||
(should (eq (ebox-style-node-specified-value node :text-align)
|
||||
'center))))
|
||||
|
||||
(ert-deftest ebox-playground-evaluates-lexical-children-and-text ()
|
||||
"Lexical closures can produce spliced child lists and dynamic text."
|
||||
(let* ((source
|
||||
"(let ((labels '(\"one\" \"two\"))
|
||||
(make-child (let ((prefix \"Item: \"))
|
||||
(lambda (label)
|
||||
`(box ,(concat prefix label))))))
|
||||
`(column :padding ((lh 0) (ch ,(length labels)))
|
||||
,@(mapcar make-child labels)))")
|
||||
(form (ebox-playground--evaluate-form
|
||||
(ebox-playground--read-form source)))
|
||||
(rendered (substring-no-properties (ebox-render (ebox-build form)))))
|
||||
(should (equal form '(column :padding ((lh 0) (ch 2))
|
||||
(box "Item: one") (box "Item: two"))))
|
||||
(should (string-match-p "Item: one" rendered))
|
||||
(should (string-match-p "Item: two" rendered))))
|
||||
|
||||
(ert-deftest ebox-playground-does-not-evaluate-quoted-property-data ()
|
||||
"A property that resembles executable code remains literal data."
|
||||
(let ((form (ebox-playground--evaluate-form
|
||||
'(quote (box :width (error "Must remain data") "Text")))))
|
||||
(should (equal form
|
||||
'(box :width (error "Must remain data") "Text")))))
|
||||
|
||||
(ert-deftest ebox-playground-rejects-multiple-or-incomplete-trailing-forms ()
|
||||
"Exactly one complete Elisp expression is allowed in a source file."
|
||||
(dolist (source '("'(box \"one\")\n'(box \"two\")"
|
||||
"'(box \"one\")\n("
|
||||
"'(box \"one\")\n\"unfinished"))
|
||||
(should-error (ebox-playground--read-form source) :type 'user-error))
|
||||
(should (equal (ebox-playground--read-form
|
||||
"; Header\n'(box \"one\") ; Trailing comment\n")
|
||||
'(quote (box "one")))))
|
||||
|
||||
(ert-deftest ebox-playground-file-and-buffer-share-elisp-evaluation ()
|
||||
"File previews and editor buffers build the same computed layout."
|
||||
(let* ((source "(let ((body \"Computed body\"))
|
||||
`(box :width (px 240) ,body))")
|
||||
(file (make-temp-file "ebox-playground-evaluation-" nil ".ebox" source)))
|
||||
(unwind-protect
|
||||
(with-temp-buffer
|
||||
(insert source)
|
||||
(ebox-dsl-mode)
|
||||
(let ((buffer-form (ebox-playground--source-form)))
|
||||
(should (equal buffer-form '(box :width (px 240) "Computed body")))
|
||||
(should (equal (ebox-render (ebox-playground-view file 720))
|
||||
(ebox-render (ebox-build buffer-form))))))
|
||||
(delete-file file))))
|
||||
|
||||
(defvar ebox-playground-test--companion-value nil
|
||||
"Dynamically isolated value written by temporary companion fixtures.")
|
||||
|
||||
(defvar ebox-playground-test--companion-loads 0
|
||||
"Number of source loads performed by a temporary companion fixture.")
|
||||
|
||||
(defun ebox-playground-test--call-with-companion-files (function)
|
||||
"Call FUNCTION with temporary DSL, companion, and preview names; clean up."
|
||||
(let* ((directory (make-temp-file "ebox-playground-companion-" t))
|
||||
(file (expand-file-name "example.ebox" directory))
|
||||
(companion (expand-file-name "example.el" directory))
|
||||
(preview (generate-new-buffer-name " *ebox-companion-test*"))
|
||||
(ebox-playground-test--companion-value nil)
|
||||
(ebox-playground-test--companion-loads 0)
|
||||
(load-history load-history))
|
||||
(unwind-protect
|
||||
(funcall function file companion preview)
|
||||
(ebox-playground-close preview)
|
||||
(ebox-playground-close (format "*Ebox Preview: %s*" file))
|
||||
(delete-directory directory t))))
|
||||
|
||||
(ert-deftest ebox-playground-companion-reloads-exact-source-before-file-evaluation ()
|
||||
"Reload an edited sibling source, regardless of bytecode or current directory."
|
||||
(ebox-playground-test--call-with-companion-files
|
||||
(lambda (file companion _preview)
|
||||
(with-temp-file file
|
||||
(insert "(list 'box ebox-playground-test--companion-value)"))
|
||||
(with-temp-file companion
|
||||
(insert ";;; -*- lexical-binding: t; -*-\n")
|
||||
(insert "(setq ebox-playground-test--companion-value \"stale bytecode\")"))
|
||||
(should (byte-compile-file companion))
|
||||
(let ((default-directory temporary-file-directory)
|
||||
(load-prefer-newer nil))
|
||||
(dolist (value '("first source" "edited source"))
|
||||
(with-temp-file companion
|
||||
(insert ";;; -*- lexical-binding: t; -*-\n")
|
||||
(prin1 `(progn
|
||||
(cl-incf ebox-playground-test--companion-loads)
|
||||
(setq ebox-playground-test--companion-value ,value))
|
||||
(current-buffer)))
|
||||
;; An ordinary suffix-selecting load would choose the newer .elc.
|
||||
(set-file-times companion
|
||||
(time-subtract (current-time) (seconds-to-time 60)))
|
||||
(should (string-match-p
|
||||
value (ebox-render (ebox-playground-view file 240))))
|
||||
(should-not (equal ebox-playground-test--companion-value
|
||||
"stale bytecode"))))
|
||||
(should (= ebox-playground-test--companion-loads 2)))))
|
||||
|
||||
(ert-deftest ebox-playground-companion-source-command-reloads-and-preserves-preview ()
|
||||
"C-c C-c loads the sibling before unsaved DSL and preserves a failed preview."
|
||||
(ebox-playground-test--call-with-companion-files
|
||||
(lambda (file companion _preview)
|
||||
(with-temp-file file (insert "'(box \"Saved disk layout\")"))
|
||||
(save-window-excursion
|
||||
(with-temp-buffer
|
||||
(setq buffer-file-name file)
|
||||
(insert "(list 'box ebox-playground-test--companion-value)")
|
||||
(ebox-dsl-mode)
|
||||
(let ((default-directory temporary-file-directory)
|
||||
(preview (format "*Ebox Preview: %s*" file))
|
||||
(ebox-viewport-width 240))
|
||||
(dolist (value '("first buffer preview" "edited buffer preview"))
|
||||
(with-temp-file companion
|
||||
(insert ";;; -*- lexical-binding: t; -*-\n")
|
||||
(prin1 `(progn
|
||||
(cl-incf ebox-playground-test--companion-loads)
|
||||
(setq ebox-playground-test--companion-value ,value))
|
||||
(current-buffer)))
|
||||
(call-interactively (key-binding (kbd "C-c C-c")))
|
||||
(with-current-buffer preview
|
||||
(should (string-match-p value (buffer-string)))
|
||||
(should-not (string-match-p "Saved disk layout" (buffer-string)))))
|
||||
(should (= ebox-playground-test--companion-loads 2))
|
||||
(let ((before (with-current-buffer preview (buffer-string)))
|
||||
(existing (get-buffer preview)))
|
||||
(with-temp-file companion
|
||||
(insert ";;; -*- lexical-binding: t; -*-\n")
|
||||
(insert "(error \"Companion source failed\")"))
|
||||
(should-error
|
||||
(call-interactively (key-binding (kbd "C-c C-c"))))
|
||||
(should (eq existing (get-buffer preview)))
|
||||
(with-current-buffer preview
|
||||
(should (equal-including-properties before (buffer-string)))))))))))
|
||||
|
||||
(ert-deftest ebox-playground-companion-file-load-error-preserves-mounted-preview ()
|
||||
"A failed companion leaves existing preview contents and region handles live."
|
||||
(ebox-playground-test--call-with-companion-files
|
||||
(lambda (file companion preview)
|
||||
(with-temp-file file
|
||||
(insert "(list 'box :id \"kept\" ebox-playground-test--companion-value)"))
|
||||
(with-temp-file companion
|
||||
(insert ";;; -*- lexical-binding: t; -*-\n")
|
||||
(insert "(setq ebox-playground-test--companion-value \"Before failure\")"))
|
||||
(let* ((buffer (ebox-playground-open-file file preview))
|
||||
(handle (ebox-region-resolve buffer "kept"))
|
||||
(before (with-current-buffer buffer (buffer-string))))
|
||||
(with-temp-file companion
|
||||
(insert ";;; -*- lexical-binding: t; -*-\n")
|
||||
(insert "(error \"Companion load failed\")"))
|
||||
(should-error (ebox-playground-open-file file preview))
|
||||
(should (eq buffer (get-buffer preview)))
|
||||
(with-current-buffer buffer
|
||||
(should (equal-including-properties before (buffer-string))))
|
||||
(ebox-region-update handle :content "Still mounted")
|
||||
(with-current-buffer buffer
|
||||
(should (string-match-p "Still mounted" (buffer-string))))))))
|
||||
|
||||
(ert-deftest ebox-playground-companion-is-optional-and-bytecode-alone-is-ignored ()
|
||||
"Unpaired files and editor buffers keep working without loading stray bytecode."
|
||||
(ebox-playground-test--call-with-companion-files
|
||||
(lambda (file companion preview)
|
||||
(with-temp-file file (insert "'(box \"Standalone example\")"))
|
||||
(with-temp-file (concat companion "c")
|
||||
(insert "(error \"A bytecode-only companion must not load\")"))
|
||||
(let ((buffer (ebox-playground-open-file file preview)))
|
||||
(with-current-buffer buffer
|
||||
(should (string-match-p "Standalone example" (buffer-string)))))
|
||||
(with-temp-buffer
|
||||
(setq buffer-file-name file)
|
||||
(insert-file-contents file)
|
||||
(ebox-dsl-mode)
|
||||
(should (equal (ebox-playground--source-form)
|
||||
'(box "Standalone example")))))))
|
||||
|
||||
(provide 'ebox-playground-tests)
|
||||
|
||||
;;; ebox-playground-tests.el ends here
|
||||
|
||||
Loading…
Reference in New Issue
Block a user