feat: add sizing and interactive text reference examples
Load same-basename Elisp companions through the generic preview runner. Demonstrate size semantics and native help, pointer, hover and keymap behavior with isolated example state. Update Flex and Grid examples and extend reusable comparison and interaction evaluators with publication, allocation and fresh-render parity checks. Validation: make check passed, including all 72 Playground tests.
This commit is contained in:
parent
046b1cf87b
commit
6cd5a67a35
23
AGENTS.md
Normal file
23
AGENTS.md
Normal file
@ -0,0 +1,23 @@
|
||||
# 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.
|
||||
@ -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)` 解析为正文区域高度,让视口相对区块适应预览窗格。
|
||||
- 触控/悬停差异:无。
|
||||
|
||||
## 交互状态
|
||||
|
||||
24
Makefile
24
Makefile
@ -7,23 +7,37 @@ EBOX_NATIVE_RELEASE_DIR = $(shell $(EMACS) -Q --batch $(LOAD_PATH) \
|
||||
-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 performance-prepare \
|
||||
.PHONY: all compile test size-tests interaction-tests check checkdoc load performance performance-prepare \
|
||||
performance-evaluator clean
|
||||
|
||||
all: check
|
||||
|
||||
compile:
|
||||
rm -f *.elc tests/*.elc
|
||||
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 '(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 native-comp-jit-compilation nil)' \
|
||||
-l tests/ebox-playground-tests.el -f ert-run-tests-batch-and-exit
|
||||
-l tests/ebox-playground-tests.el -l tests/ebox-playground-size-tests.el \
|
||||
-l tests/ebox-playground-interaction-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) \
|
||||
@ -47,9 +61,9 @@ performance-evaluator:
|
||||
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
|
||||
|
||||
267
README.md
267
README.md
@ -1,24 +1,283 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
<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
|
||||
|
||||
198
README.zh-CN.md
198
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.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
|
||||
|
||||
@ -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 ()
|
||||
@ -168,9 +163,7 @@ CANVAS-BACKGROUND fills display space outside generated root lines."
|
||||
(unwind-protect
|
||||
(prog1
|
||||
(progn
|
||||
(ebox-playground--apply-preview-display-settings
|
||||
(ebox-style-node-specified-value
|
||||
input :background-color))
|
||||
(ebox-playground--apply-preview-display-settings)
|
||||
(ebox-render-to-buffer buffer input))
|
||||
(setq published-p t))
|
||||
(unless published-p
|
||||
@ -184,57 +177,35 @@ Visible mounted buffers follow their window through Ebox's viewport controller."
|
||||
(ebox-playground--effective-viewport-width))))
|
||||
(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
|
||||
@ -244,13 +215,14 @@ 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)
|
||||
|
||||
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
|
||||
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."))
|
||||
@ -56,7 +56,8 @@
|
||||
(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--line-widths (buffer)
|
||||
@ -65,20 +66,176 @@
|
||||
(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"
|
||||
@ -86,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.
|
||||
|
||||
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
|
||||
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,99 +61,185 @@
|
||||
"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 '(min-content max-content)"
|
||||
":grid-template-columns '((120) (74))"
|
||||
":padding '(0 (2))"
|
||||
":grid-template-columns '((192))"
|
||||
":grid-template-columns '((88) (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 '((64))"
|
||||
":grid-auto-columns 'auto"
|
||||
":grid-auto-columns '(fr 1)"
|
||||
":grid-auto-columns '((minmax"
|
||||
":grid-auto-rows 2"
|
||||
":grid-auto-rows 'auto"
|
||||
":grid-auto-rows '(fr 1)"
|
||||
":grid-auto-rows '((minmax"
|
||||
":grid-auto-flow 'row"
|
||||
":grid-auto-flow 'column"
|
||||
":gap 1"
|
||||
":gap '(1 (14))"
|
||||
":row-gap 2"
|
||||
":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"
|
||||
":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
|
||||
(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"
|
||||
@ -151,7 +249,9 @@
|
||||
(insert-file-contents
|
||||
(expand-file-name "Makefile" ebox-playground-directory))
|
||||
(buffer-string))))
|
||||
(should (string-match-p "ebox-buffer-set-observer" source))
|
||||
(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))
|
||||
@ -162,7 +262,7 @@
|
||||
(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) source)))))
|
||||
(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."
|
||||
@ -241,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))
|
||||
@ -270,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
|
||||
@ -317,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))
|
||||
@ -339,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)
|
||||
@ -358,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))
|
||||
@ -462,8 +751,8 @@
|
||||
(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)))
|
||||
@ -472,8 +761,8 @@
|
||||
(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
|
||||
@ -506,52 +795,89 @@
|
||||
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
|
||||
@ -572,21 +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* ((input (ebox-playground-view))
|
||||
(root (car (ebox-canonical-input-roots input)))
|
||||
(gallery-width
|
||||
(plist-get (car (ebox-box-node-children root)) :width))
|
||||
"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."
|
||||
@ -603,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)))
|
||||
@ -613,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-inline-start)
|
||||
'(4)))
|
||||
'(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