Compare commits
11 Commits
rollback-g
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af9803f8aa | ||
|
|
b94e19ad7a | ||
|
|
ecc490d8c6 | ||
|
|
db41a9b769 | ||
|
|
8631531ebb | ||
|
|
40a6b36bcc | ||
|
|
834c05f18b | ||
|
|
6cd5a67a35 | ||
|
|
046b1cf87b | ||
|
|
cdefca440e | ||
|
|
65a24e2b69 |
36
AGENTS.md
Normal file
36
AGENTS.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# 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, viewport height, and optional options (`:region-id` selects a semantic nested owner; `:cold t` measures forward steps without prewarming). It measures publication and verifies committed-input fresh-render parity while preserving existing GUI previews. Consult this entry point before adding another scroll probe, and extend it when the diagnostic coverage is missing. See README for the invocation and measurement boundaries.
|
||||||
|
|
||||||
|
For repeated interaction behavior or command performance, use `ebox-playground-interaction-evaluator-run` from `scripts/ebox-playground-flex-resize-evaluator.el`. Pass the `.ebox` file and visible control label, with optional key, sample count, width, and height. It performs native key lookup without repositioning after updates, reports command-loop timings/GC/publications and layout/projection work counters, and verifies committed-snapshot render parity. Read fragment call counts with their input-character totals to distinguish local slices from full-surface work. For native click dispatch in an already visible buffer, use the same file's `ebox-playground-interaction-evaluator-click` with buffer, label, and optional in-label offset; it resolves an Emacs event and calls the interactive command, not an OS mouse click. Consult and extend these entry points before adding another interaction probe; see README for inputs, cleanup, the between-sample deadline, and measurement limits.
|
||||||
|
|
||||||
|
Keep intentional spacer height explicit: use `(box :height (lh 1))` for a one-line separator, omitting width when ordinary block sizing already fills its parent. An otherwise empty Box with automatic height has zero content height.
|
||||||
|
|
||||||
|
For size declarations, consult the [shared Ebox unit-constraint table](../ebox/docs/user/ebox-user-guide.en.md#size-unit-constraints). Do not duplicate its matrix in a Playground-specific parser or guideline. Check the property's axis, every nested function branch, both axes after shorthand expansion, and the separate border-paint rule. Invalid combinations must fail at initial construction; examples must not depend on cross-axis conversion.
|
||||||
|
|
||||||
|
For size, unit, keyword, or sizing-function demonstration work, maintain `examples/size-reference.ebox` and run `make size-tests` (with `EMACS` set to the intended executable). Extend `tests/ebox-playground-size-tests.el` for coverage or numeric regressions instead of creating another size gallery or verification script.
|
||||||
|
|
||||||
|
Keep necessary business functions and variables in a same-basename `.el` file beside the `.ebox` layout. The generic runner loads that exact companion source, when present, before every preview evaluation, including `C-c C-c` and `ebox-playground-open-file`. Keep the `.ebox` focused on layout and its data references; do not add helpers merely to hide repeated property lists. Reload state according to ordinary Elisp definitions, and keep each companion lexically bound.
|
||||||
|
|
||||||
|
For native help, pointer, hover-style, or keymap demonstration work, maintain `examples/interaction-reference.el` and `examples/interaction-reference.ebox`, then run `make interaction-tests` (with `EMACS` set to the intended executable). Extend `tests/ebox-playground-interaction-tests.el` for capability, command, replacement, and removal regressions. Use `ebox-help-create` and `ebox-keymap-create` for zero-argument business callbacks and native help/command buffer context, and use `ebox-region-update` with semantic IDs in that buffer. These reusable adapters belong to Ebox; moving local adapters into the companion is not a substitute. Raw native callbacks and keymaps remain supported and own their event-context handling. Extend the generic runner tests for companion reload behavior rather than adding an example-specific loading mechanism.
|
||||||
|
|
||||||
|
For layer, overlap, positioning, or anchored-menu demonstrations, maintain `examples/layer-reference.ebox` and its same-basename companion, preserve `examples/layer-minimal.ebox` as the minimal fixture, and run `make layer-tests`. Extend `tests/ebox-playground-layer-tests.el` for actual native commands and committed-snapshot render parity instead of creating another demo runner or probe. Keep layout in the DSL, business state/actions in the companion, and one font size throughout positioned content. Use the shared interaction evaluator above for repeated layer-command timing. Each parent creates its local stacking group automatically; use `:layer root` on a nested absolute node when root projection is required, with semantic `:anchor` and `:placement` declarations for anchored menus.
|
||||||
|
|
||||||
|
For direct-manipulation desktop windows, click-to-front, title-bar dragging,
|
||||||
|
complete light/dark palettes, and desktop-only mouse selection policy,
|
||||||
|
maintain `examples/desktop-reference.ebox` with its companion and run
|
||||||
|
`make desktop-tests`. Reuse `tests/ebox-playground-desktop-tests.el` for native
|
||||||
|
press/motion/release streams, gap/end-of-buffer selection fallbacks, palette
|
||||||
|
round trips, intermediate publication and fresh-render parity.
|
||||||
|
Use the shared interaction evaluator with the desktop's `"01 ST"` dock label
|
||||||
|
and `"M-<right>"` for repeated position publication timing. Keep native gesture
|
||||||
|
state in the companion, use public Ebox updates, and preserve the existing layer
|
||||||
|
reference instead of turning the generic runner into a window manager.
|
||||||
56
DESIGN.md
56
DESIGN.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Source of truth
|
## Source of truth
|
||||||
- Status: Active
|
- Status: Active
|
||||||
- Last refreshed: 2026-08-05
|
- Last refreshed: 2026-09-11
|
||||||
- Primary product surfaces: The `.ebox` files under `examples/`, the buffer opened by `ebox-playground-open`, and the generic file mode.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
## Product goals
|
## Product goals
|
||||||
- Goals: Demonstrate Ebox Grid and composition in a polished standalone example; give the low-level package a visual quality bar consistent with the Flex reference.
|
- Goals: Demonstrate Ebox Grid and composition in a polished standalone example; give the low-level package a visual quality bar consistent with the Flex reference.
|
||||||
- Non-goals: ETAF Components, application state, browser imitation, or a second Playground framework.
|
- Non-goals: ETAF Components, a general application framework, or a second Playground runner. Interactive examples keep their own small state in a same-basename companion.
|
||||||
- Success signals: The first frame clearly communicates hierarchy and Grid behavior; every tinted block has readable text; no content crosses the visible viewport.
|
- Success signals: The first frame clearly communicates hierarchy and Grid behavior; every tinted block has readable text; no content crosses the visible viewport.
|
||||||
|
|
||||||
## Personas and jobs
|
## Personas and jobs
|
||||||
@ -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.
|
- 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.
|
- 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.
|
- 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
|
## Accessibility
|
||||||
- Target standard: High contrast and readable labels in ordinary GUI Emacs themes.
|
- Target standard: High contrast and readable labels in ordinary GUI Emacs themes.
|
||||||
@ -55,7 +57,7 @@
|
|||||||
|
|
||||||
## Responsive behavior
|
## Responsive behavior
|
||||||
- Supported breakpoints/devices: GUI Emacs windows from compact split previews to fullscreen desktop widths.
|
- 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.
|
- Touch/hover differences: None.
|
||||||
|
|
||||||
## Interaction states
|
## Interaction states
|
||||||
@ -80,3 +82,49 @@
|
|||||||
|
|
||||||
## Open questions
|
## Open questions
|
||||||
- [ ] Add more standalone `.ebox` fixtures only when each teaches a distinct public Ebox responsibility / maintainer / prevents fixture sprawl.
|
- [ ] Add more standalone `.ebox` fixtures only when each teaches a distinct public Ebox responsibility / maintainer / prevents fixture sprawl.
|
||||||
|
|
||||||
|
## Parallax desktop example
|
||||||
|
|
||||||
|
- Surface: `examples/desktop-reference.ebox` and its companion. This dedicated
|
||||||
|
direct-manipulation example complements the existing static/property-oriented
|
||||||
|
layer reference; it does not replace it.
|
||||||
|
- Job: move five overlapping text windows, reveal covered content, and bring any
|
||||||
|
window forward by clicking its exposed body/title or its persistent dock entry.
|
||||||
|
- Visual direction: midnight blue workspace, cool readable ink, violet Studio,
|
||||||
|
cyan Signal, amber Notes, mint Terminal and rose Palette accents. One configured
|
||||||
|
font size throughout. Use contrasting title bars and compact pane content,
|
||||||
|
rather than fake close controls, images or additional font packages.
|
||||||
|
- Light theme: a warm pale shell, blue-gray workspace, and five pastel card
|
||||||
|
bodies with dark ink. Retune every caption, active/inactive title, dock entry,
|
||||||
|
waveform, artwork and swatch; changing only the workspace background is not
|
||||||
|
an acceptable light theme. Keep text contrast strong and decorative dots quiet.
|
||||||
|
Palette declarations stay in the DSL; semantic classes connect them to the
|
||||||
|
companion's public selector/region updates, without a global stylesheet.
|
||||||
|
- Structure: compact identity/utility bar, clipped desktop with five independent
|
||||||
|
sibling windows, then persistent dock and status. Keep the workspace dominant;
|
||||||
|
each pane contains a distinct editorial/code/task/signal composition.
|
||||||
|
- Interaction: raise on press, preserve grab offset, repaint changed positions
|
||||||
|
during native mouse tracking, apply the final release position, and retain the
|
||||||
|
last accepted position after interruption. No inertial or decorative animation.
|
||||||
|
The current Ebox layer contract uses pixel X and host-text-row Y coordinates.
|
||||||
|
- Mouse selection: gaps, background rows, line endings and buffer end are desktop
|
||||||
|
surfaces too. A desktop-only minor mode remaps native selection fallbacks;
|
||||||
|
control keymaps keep their click/drag behavior. Remove the policy on unmount
|
||||||
|
or replacement by another example. Do not change global mouse bindings.
|
||||||
|
- Keyboard: RET/SPC activates a pane or dock entry; title-bar Alt+arrows move it.
|
||||||
|
Reset restores the original arrangement. No changes to global Emacs bindings.
|
||||||
|
- LIGHT/DARK toggles the complete palette in one publication and keeps geometry,
|
||||||
|
stacking and active window. Reset keeps the chosen theme; fresh previews start
|
||||||
|
dark and keep independent theme state.
|
||||||
|
- Responsive behavior: use viewport-relative positions and bounded pane widths;
|
||||||
|
overflow is clipped at the desktop boundary. Dock entries remain available for
|
||||||
|
covered panes. Target at least 40 columns and 20 text rows; smaller workspaces
|
||||||
|
prioritize the dock, and reopening/resetting restores the initial arrangement.
|
||||||
|
- State and failure: one independent companion state per preview. Commit state
|
||||||
|
only after public Ebox publication succeeds. Mouse tracking owns no persistent
|
||||||
|
timer, advice or global map and releases its transient state on exit/error/quit.
|
||||||
|
- Verification: real commands retrieved from rendered native keymaps, synthetic
|
||||||
|
motion/release streams with intermediate-publication checks, stacking/covered
|
||||||
|
ownership and committed-input render parity, plus compact and wide renders.
|
||||||
|
The standing restriction on operating the user's GUI Emacs remains in force;
|
||||||
|
batch checks do not establish native redisplay latency or screenshot fidelity.
|
||||||
|
|||||||
@ -2,10 +2,37 @@
|
|||||||
|
|
||||||
## 真相来源
|
## 真相来源
|
||||||
- 状态:有效
|
- 状态:有效
|
||||||
- 最近更新:2026-08-05
|
- 最近更新:2026-09-11
|
||||||
- 主要产品界面:`examples/` 下的 `.ebox` 文件、`ebox-playground-open` 打开的缓冲区,以及通用文件模式。
|
- 主要产品界面:`examples/` 下的 `.ebox` 文件、`ebox-playground-open` 打开的缓冲区,以及通用文件模式。
|
||||||
- 已审阅证据:`ebox-playground.el`、`examples/` 下的 `.ebox` fixture、`tests/ebox-playground-tests.el`、`README.md`、`README.zh-CN.md` 以及画廊/参考文件的实际渲染。
|
- 已审阅证据:`ebox-playground.el`、`examples/` 下的 `.ebox` fixture、`tests/ebox-playground-tests.el`、`README.md`、`README.zh-CN.md` 以及画廊/参考文件的实际渲染。
|
||||||
|
|
||||||
|
## Parallax 桌面示例
|
||||||
|
|
||||||
|
`examples/desktop-reference.ebox` 与同名 companion 单独演示直接操作,保留原有图层参考页。
|
||||||
|
深蓝工作台中有五个窗口:紫色 Studio、纸色 Notes、青色 Signal、薄荷色 Terminal、玫瑰色
|
||||||
|
Palette。各窗口有独立内容和局部层叠,全部使用当前字体大小;标题栏与底部窗口栏标出
|
||||||
|
活动窗口,不添加假的关闭按钮或自动动画。
|
||||||
|
|
||||||
|
亮色主题使用暖白外框、浅蓝灰工作区和五种浅色卡片,搭配深色正文。标题栏、窗口入口、
|
||||||
|
辅助文字、波形、嵌套图案与色板都单独适配,不能只把工作台背景换亮。配色表保留在
|
||||||
|
DSL 中,以语义 class 连接 companion 的公开 selector/region 更新,不使用全局样式表。
|
||||||
|
|
||||||
|
点击露出的标题、正文或底部窗口入口时整窗置顶;按住标题栏拖拽时即时发布位置,保留
|
||||||
|
鼠标抓取偏移,并应用松手的最终位置。横向按像素,纵向遵循文本行网格。中断保留最后
|
||||||
|
成功的位置,手势退出不留下 timer、advice 或全局键位。RET/SPC 激活,标题或窗口栏的
|
||||||
|
Alt+方向键移动窗口,RESET 恢复原始位置与次序。业务状态只在 Ebox 发布成功后提交,
|
||||||
|
每个预览独立。
|
||||||
|
|
||||||
|
LIGHT/DARK 在一次发布中切换完整配色,保留位置、层次和活动窗口;RESET 不改变主题。
|
||||||
|
新预览默认深色。Desktop 局部 minor mode 拦截背景、按钮间隙、换行和缓冲区末尾的原生
|
||||||
|
鼠标选区命令,保留按钮与标题栏自身的行为。卸载或切换成其他示例后移除该策略,
|
||||||
|
不修改 Emacs 的全局鼠标绑定。
|
||||||
|
|
||||||
|
结构为紧凑页头与工具栏、裁剪的主工作区、固定窗口栏和坐标状态;推荐至少 40 列、
|
||||||
|
20 行。较小尺寸保留窗口入口,RESET 可找回拖出边界的窗口。测试覆盖真实 keymap
|
||||||
|
命令、中间运动帧、松手、遮挡归属、失败回滚及独立渲染一致性。遵守不操作用户 GUI
|
||||||
|
Emacs 的现有限制,批处理验证不等同于原生重绘延迟或截图验收。
|
||||||
|
|
||||||
## 品牌
|
## 品牌
|
||||||
- 个性:精确、现代、克制、适合教学。
|
- 个性:精确、现代、克制、适合教学。
|
||||||
- 信任信号:像素对齐的分区、准确的 Grid 放置、明确的文字对比度、只使用公开 API,以及不依赖主题 face 也清晰的布局。
|
- 信任信号:像素对齐的分区、准确的 Grid 放置、明确的文字对比度、只使用公开 API,以及不依赖主题 face 也清晰的布局。
|
||||||
@ -13,7 +40,7 @@
|
|||||||
|
|
||||||
## 产品目标
|
## 产品目标
|
||||||
- 目标:用一个精致的独立示例演示 Ebox Grid 与组合能力,让底层包达到 Flex 参考示例的视觉质量标准。
|
- 目标:用一个精致的独立示例演示 Ebox Grid 与组合能力,让底层包达到 Flex 参考示例的视觉质量标准。
|
||||||
- 非目标:ETAF Component、应用状态、浏览器仿制品,或第二套 Playground 框架。
|
- 非目标:ETAF Component、通用应用框架,或第二套 Playground 运行器。交互示例通过同名 companion 维护自己的小型状态。
|
||||||
- 成功信号:第一屏能清楚表达层级和 Grid 行为;每个带色区块的文字都清晰可读;没有内容越过可见视口。
|
- 成功信号:第一屏能清楚表达层级和 Grid 行为;每个带色区块的文字都清晰可读;没有内容越过可见视口。
|
||||||
|
|
||||||
## 用户与任务
|
## 用户与任务
|
||||||
@ -44,7 +71,9 @@
|
|||||||
- 复用的作者 form:String、`text`、`box`、`row`、`column`、`flex` 和 `grid`。空 `box` 直接表达间距,不再引入另一个公共 form。
|
- 复用的作者 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 主题下保持高对比度和标签可读性。
|
- 目标标准:在常见 GUI Emacs 主题下保持高对比度和标签可读性。
|
||||||
@ -55,7 +84,7 @@
|
|||||||
|
|
||||||
## 响应式行为
|
## 响应式行为
|
||||||
- 支持的断点/设备:正文宽度至少为 760 px 的 GUI Emacs 窗口,从紧凑窗口到全屏桌面。
|
- 支持的断点/设备:正文宽度至少为 760 px 的 GUI Emacs 窗口,从紧凑窗口到全屏桌面。
|
||||||
- 布局适配:画廊统一拥有一个固定的 720 px 内容画布;嵌套 Grid 和 Flex 通过公开 Ebox 布局行为划分该宽度,而不是各自读取视口。
|
- 布局适配:独立画廊默认使用 720 px 内容画布;分屏 `.ebox` 预览将 `(vw 100)` 解析为预览窗格的显示安全宽度,将 `(vh 100)` 解析为正文区域高度,让视口相对区块适应预览窗格。
|
||||||
- 触控/悬停差异:无。
|
- 触控/悬停差异:无。
|
||||||
|
|
||||||
## 交互状态
|
## 交互状态
|
||||||
|
|||||||
38
Makefile
38
Makefile
@ -7,23 +7,51 @@ EBOX_NATIVE_RELEASE_DIR = $(shell $(EMACS) -Q --batch $(LOAD_PATH) \
|
|||||||
-l ../ebox/ebox-native-reflow.el \
|
-l ../ebox/ebox-native-reflow.el \
|
||||||
--eval '(princ (expand-file-name (format "../ebox/native/target/%s/release/" (ebox-native-reflow--rust-target))))')
|
--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 layer-tests desktop-tests check checkdoc load performance performance-prepare \
|
||||||
performance-evaluator clean
|
performance-evaluator clean
|
||||||
|
|
||||||
all: check
|
all: check
|
||||||
|
|
||||||
compile:
|
compile:
|
||||||
rm -f *.elc tests/*.elc
|
rm -f *.elc tests/*.elc examples/*.elc
|
||||||
$(EMACS) -Q --batch $(LOAD_PATH) \
|
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||||
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil byte-compile-error-on-warn t)' \
|
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil byte-compile-error-on-warn t)' \
|
||||||
--eval '(load-file "ebox-playground.el")' \
|
--eval '(load-file "ebox-playground.el")' \
|
||||||
--eval '(byte-compile-file "ebox-playground.el")' \
|
--eval '(byte-compile-file "ebox-playground.el")' \
|
||||||
|
--eval '(dolist (file (directory-files "examples" t)) (when (string-suffix-p ".el" file) (unless (byte-compile-file file) (error "Companion compilation failed: %s" file))))' \
|
||||||
--eval '(byte-compile-file "scripts/ebox-playground-flex-resize-evaluator.el")'
|
--eval '(byte-compile-file "scripts/ebox-playground-flex-resize-evaluator.el")'
|
||||||
|
|
||||||
test: compile
|
test: compile
|
||||||
$(EMACS) -Q --batch $(LOAD_PATH) \
|
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||||
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
--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 -l tests/ebox-playground-layer-tests.el \
|
||||||
|
-l tests/ebox-playground-desktop-tests.el \
|
||||||
|
-f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
desktop-tests: compile
|
||||||
|
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||||
|
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
||||||
|
-l tests/ebox-playground-desktop-tests.el \
|
||||||
|
--eval '(ert-run-tests-batch-and-exit "^ebox-playground-desktop-")'
|
||||||
|
|
||||||
|
layer-tests: compile
|
||||||
|
$(MAKE) -C ../ebox layer-tests EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
|
||||||
|
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||||
|
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
||||||
|
-l tests/ebox-playground-layer-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
interaction-tests: compile
|
||||||
|
$(MAKE) -C ../ebox interaction-tests EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
|
||||||
|
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||||
|
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
||||||
|
-l tests/ebox-playground-interaction-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
size-tests: compile
|
||||||
|
$(MAKE) -C ../ebox size-tests EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
|
||||||
|
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||||
|
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
||||||
|
-l tests/ebox-playground-size-tests.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
load: compile
|
load: compile
|
||||||
$(EMACS) -Q --batch $(LOAD_PATH) \
|
$(EMACS) -Q --batch $(LOAD_PATH) \
|
||||||
@ -47,9 +75,9 @@ performance-evaluator:
|
|||||||
EMACS="$(EMACS)" ./scripts/flex-resize-evaluator.sh
|
EMACS="$(EMACS)" ./scripts/flex-resize-evaluator.sh
|
||||||
|
|
||||||
checkdoc:
|
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
|
check: checkdoc compile test
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f *.elc tests/*.elc
|
rm -f *.elc tests/*.elc examples/*.elc
|
||||||
|
|||||||
418
README.md
418
README.md
@ -1,24 +1,434 @@
|
|||||||
# ebox-playground
|
# ebox-playground
|
||||||
|
|
||||||
`ebox-playground` is the independent, generic file runner for Ebox DSL examples. It uses only public Ebox APIs; concrete layouts live in readable `.ebox` fixtures under `examples/`. The migrated low-level fixtures include the original Basic, Comprehensive, Flex, and Responsive references, while `grid-reference.ebox` provides a complete public Grid property/value gallery with the same restrained terracotta, sage, blue, violet, ochre, and teal visual language.
|
`ebox-playground` is the independent, generic file runner for Ebox DSL examples. It uses only public Ebox APIs; concrete layouts live in readable `.ebox` fixtures under `examples/`. `flex-reference.ebox`, `grid-reference.ebox`, and `size-reference.ebox` demonstrate layout and sizing with a restrained terracotta, sage, blue, violet, ochre, and teal visual language. `interaction-reference.ebox` demonstrates native Ebox interaction and retained updates. `layer-reference.ebox` combines retained overlapping panels with a native anchored menu. `desktop-reference.ebox` provides five directly draggable, overlapping text windows.
|
||||||
|
|
||||||
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.
|
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
|
```elisp
|
||||||
(require 'ebox-playground)
|
(require 'ebox-playground)
|
||||||
(ebox-playground-open)
|
(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.
|
||||||
|
|
||||||
|
## PARALLAX draggable desktop
|
||||||
|
|
||||||
|
Open `examples/desktop-reference.ebox` and press `C-c C-c`, or use:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-playground-open-file
|
||||||
|
(expand-file-name "examples/desktop-reference.ebox"
|
||||||
|
ebox-playground-directory))
|
||||||
|
```
|
||||||
|
|
||||||
|
Five independent panes share a midnight-blue desktop: violet Studio, paper
|
||||||
|
Notes, cyan Signal, mint Terminal, and rose Palette. Each contains its own
|
||||||
|
text and layered artwork. All text uses the configured font size.
|
||||||
|
|
||||||
|
- Click any exposed pane, including its blank body area, to bring the entire
|
||||||
|
window forward. Other windows keep their relative stacking order.
|
||||||
|
- Hold the primary button on a title bar and move it. Positions are published
|
||||||
|
while the button is held, preserving the initial grab offset; release applies
|
||||||
|
the final position. Windows can partially or fully cover one another.
|
||||||
|
- The five dock entries raise windows even when they are completely covered.
|
||||||
|
**CYCLE** brings the backmost window forward; **RESET** restores all initial
|
||||||
|
positions and the original stack, including panes clipped off the desktop.
|
||||||
|
- **LIGHT/DARK** switches the complete palette: workspace, all five card bodies,
|
||||||
|
text, title bars, dock, artwork, swatches and hover colors. Light mode uses
|
||||||
|
pastel cards with dark ink. Window positions and stacking survive a toggle;
|
||||||
|
**RESET** keeps the chosen theme.
|
||||||
|
- Mouse clicks and drags do not select text, including between dock buttons and
|
||||||
|
at line/buffer ends. This policy belongs only to the Desktop preview; ordinary
|
||||||
|
buffers and other examples keep their normal mouse behavior.
|
||||||
|
- `RET`/`SPC` activates the control at point. `Alt+arrows` on a title or dock
|
||||||
|
entry moves its pane; point follows the same native control for key repeat.
|
||||||
|
`C-g` stops a drag and keeps the last accepted position.
|
||||||
|
|
||||||
|
Horizontal dragging uses pixel positions; vertical movement follows Ebox's
|
||||||
|
host text rows. This is live text-window composition, not sub-row raster motion.
|
||||||
|
Use at least 40 columns and 20 rows; larger previews leave more room to arrange
|
||||||
|
the five panes. Reopening the file creates an independent desktop state.
|
||||||
|
|
||||||
|
Run `make desktop-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs` for
|
||||||
|
native command, intermediate motion, release, overlap, rollback, independent
|
||||||
|
preview and render-parity regressions. Normal `make check` includes this suite.
|
||||||
|
For repeated movement publication timing, use the shared interaction evaluator
|
||||||
|
with this file, dock label `"01 ST"` and key `"M-<right>"`; it measures the same
|
||||||
|
position publisher without OS event delivery or GUI redisplay.
|
||||||
|
|
||||||
|
## ORBIT spatial desktop and layer lab
|
||||||
|
|
||||||
|
`examples/layer-minimal.ebox` is the smallest static overlap example. The complete
|
||||||
|
lab opens with **ORBIT / CREATIVE OS**, a dark spatial desktop with violet,
|
||||||
|
ice-blue, and acid-green cards, offset shadow slabs, nested geometric artwork,
|
||||||
|
and floating badges. It uses the same generic runner and a companion for
|
||||||
|
application state:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-playground-open-file
|
||||||
|
(expand-file-name "examples/layer-reference.ebox"
|
||||||
|
ebox-playground-directory))
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit the `.ebox` file and press `C-c C-c` for the source/preview split. Every
|
||||||
|
control supports click, `RET`, and `SPC`, with native help and hover paint.
|
||||||
|
All text uses one font size.
|
||||||
|
Toolbar hover paint belongs to each button, including its own padding; gaps
|
||||||
|
and unused row space stay unchanged. Deliberate block interaction areas, such
|
||||||
|
as the covered note and root menu choices, retain their own hover paint.
|
||||||
|
|
||||||
|
- **EXPLODE STACK** spreads the three cards apart and returns them to their
|
||||||
|
original positions without moving the following sections.
|
||||||
|
- **CYCLE FOCUS** brings the studio, task card, and player to the front in turn.
|
||||||
|
- **SWITCH MOOD** changes the card palette between lunar violet/ice/lime and
|
||||||
|
solar pink/apricot/mint. Each preview has independent state.
|
||||||
|
- **UPDATE COVERED NOTE** changes a note fully covered by Panel A. Update it
|
||||||
|
twice, then **TOGGLE PANEL A** to reveal revision 02. The revealed note has
|
||||||
|
its own native action and live help; covering it never removes its logical
|
||||||
|
data or bindings.
|
||||||
|
- **MOVE PANEL A** changes `:left` and `:top` without adding flow rows.
|
||||||
|
**RAISE / LOWER A** changes `:z-index` between 2 and 4 while Panel B stays at
|
||||||
|
3. Opaque panel content and padding replace the lower layer's visible paint.
|
||||||
|
- The relative marker reserves its original flow row and paints one row lower.
|
||||||
|
Each parent forms a local stacking group automatically; no isolation
|
||||||
|
declaration is needed.
|
||||||
|
- **OPEN / CLOSE ROOT MENU** shows a menu whose logical owner clips to one
|
||||||
|
row. The nested absolute menu declares `:layer root` and anchors to the
|
||||||
|
trigger's semantic ID, so it can paint beyond that local clip.
|
||||||
|
**CYCLE PLACEMENT** visits `bottom-start`, `bottom-end`, `top-start`, and
|
||||||
|
`top-end`; **MOVE ANCHOR** moves the trigger and its open menu together.
|
||||||
|
Choosing sage or amber updates the result and closes the menu.
|
||||||
|
|
||||||
|
`layer-reference.ebox` contains layout data and state references;
|
||||||
|
`layer-reference.el` owns state and business actions. It uses only public
|
||||||
|
`ebox-keymap-create`, `ebox-help-create`, `ebox-region-update`, and
|
||||||
|
`ebox-call-with-update-batch` APIs. Each action groups its region changes into
|
||||||
|
one batch, including the status update. The batch helper joins an existing
|
||||||
|
batch without flushing and otherwise publishes its pending changes once.
|
||||||
|
Its rollback covers queued Ebox updates, not arbitrary changes to the
|
||||||
|
companion's business state; see the [batch API
|
||||||
|
contract](../ebox/docs/user/ebox-api-reference.en.md#grouping-region-updates).
|
||||||
|
Offsets use inline `ch` and block `lh` units. The text renderer composes whole
|
||||||
|
rows, and positioned content must share its host's line height.
|
||||||
|
|
||||||
|
Run the batch checks without opening GUI windows:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make layer-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||||||
|
```
|
||||||
|
|
||||||
|
This target runs core layer contracts and the actual demo's native commands,
|
||||||
|
covered updates, movement, stacking, all four anchor placements, cross-buffer
|
||||||
|
mouse dispatch, and independent previews. After each command, the tests compare
|
||||||
|
the mounted output with a fresh render of the committed snapshot, including
|
||||||
|
paint and native capabilities. `make check` includes the Playground tests.
|
||||||
|
For repeated command timing, use the existing interaction evaluator with
|
||||||
|
`"examples/layer-reference.ebox"` and a toolbar label such as
|
||||||
|
`"UPDATE COVERED NOTE"`; see the invocation below.
|
||||||
|
|
||||||
|
For the spatial desktop's expand/collapse path, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
emacs -Q --batch -L . -L ../ebox -L ../ecss -L ../tp \
|
||||||
|
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
||||||
|
-l scripts/ebox-playground-flex-resize-evaluator.el \
|
||||||
|
--eval '(let ((r (ebox-playground-interaction-evaluator-run "examples/layer-reference.ebox" "EXPLODE STACK" "RET" 6 104 100))) (cl-remf r :text) (pp r))'
|
||||||
|
```
|
||||||
|
|
||||||
|
With the root menu closed, the three desktop toolbar commands publish the
|
||||||
|
affected layer host and status text together, without a full candidate render.
|
||||||
|
The layer host still recomposes its stacking group; this is owner-level
|
||||||
|
incremental work, not a minimal pixel rectangle. Visible root portals and
|
||||||
|
content-dependent geometry keep the broader composition fallback. Regression
|
||||||
|
tests require one publication per command and reject full candidate rendering;
|
||||||
|
wall-clock timings are diagnostic, not a CI threshold.
|
||||||
|
|
||||||
|
<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. Planning also runs for local patches; `:full-surface-renders`
|
||||||
|
separately counts full candidate renders. `: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
|
```elisp
|
||||||
(add-to-list 'auto-mode-alist '("\\.ebox\\'" . ebox-dsl-mode))
|
(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, viewport height in lines, and an optional options plist.
|
||||||
|
By default, 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, measurement-loop GC count/time,
|
||||||
|
full root renders, general document preparations, surface-plan construction,
|
||||||
|
node style visits, style computations, prefix rendering, full content materialization, 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.
|
||||||
|
|
||||||
|
Pass `(:region-id "log")` to measure a nested scroll box declared with
|
||||||
|
`:id "log"`. Pass `(:cold t)` to measure consecutive forward steps from the
|
||||||
|
initial render without materializing or warming the content first; combine
|
||||||
|
both options for a nested cold run. Cold mode requires enough forward scroll
|
||||||
|
capacity for the requested samples and does not seek to the midpoint. The
|
||||||
|
initial render can already include lookahead rows; use enough steps to cross
|
||||||
|
that prefix and inspect `:prefix-renders` and `:materializations` when measuring
|
||||||
|
a cache miss. Completed-cache no-ops do not count as materialization. The
|
||||||
|
fresh oracle renders the committed input with its scroll offsets, preserving
|
||||||
|
callback and keymap identity. It does not reload companion application state.
|
||||||
|
These checks cover Ebox publication work; use an actual displayed window when
|
||||||
|
measuring input delivery and Emacs redisplay latency.
|
||||||
|
Inspect `:document-preparations` and `:surface-plans` to distinguish a viewport
|
||||||
|
publication from scrolling that still enters the general document update path.
|
||||||
|
|
||||||
|
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:
|
The reference fixtures live directly under `examples/`. For example:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
|
|||||||
291
README.zh-CN.md
291
README.zh-CN.md
@ -1,24 +1,307 @@
|
|||||||
# ebox-playground
|
# 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。
|
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
|
```elisp
|
||||||
(require 'ebox-playground)
|
(require 'ebox-playground)
|
||||||
(ebox-playground-open)
|
(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` 也包含此示例的回归测试。
|
||||||
|
|
||||||
|
## PARALLAX 可拖拽桌面
|
||||||
|
|
||||||
|
打开 `examples/desktop-reference.ebox`,按 `C-c C-c`,或执行:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ebox-playground-open-file
|
||||||
|
(expand-file-name "examples/desktop-reference.ebox"
|
||||||
|
ebox-playground-directory))
|
||||||
|
```
|
||||||
|
|
||||||
|
深蓝工作台中放置五个内容各异的窗口:Studio、Notes、Signal、Terminal、Palette。
|
||||||
|
点击露出的标题、正文或空白区域,会将整窗置顶;按住标题栏拖动时持续更新位置,
|
||||||
|
保留抓取偏移,松手时应用最终位置。窗口可以部分或完全覆盖其他窗口,原有内容始终保留。
|
||||||
|
|
||||||
|
底部五个窗口入口可以找回被完全遮住的窗口;**CYCLE** 将最下面的窗口放到最上面,
|
||||||
|
**RESET** 恢复原始位置和次序,也能找回拖出桌面裁剪边界的窗口。RET/SPC 激活当前
|
||||||
|
控件,标题或窗口入口上的 Alt+方向键移动对应窗口并支持连续按键;C-g 结束拖拽,
|
||||||
|
保留最后成功发布的位置。每次打开的预览状态互相独立。
|
||||||
|
|
||||||
|
**LIGHT/DARK** 切换整套配色:五张卡片在亮色模式下都使用浅色底与深色文字,标题栏、
|
||||||
|
窗口入口、装饰图案、波形、色板和悬停颜色也同步适配。切换保留窗口位置与层次,
|
||||||
|
**RESET** 保留所选主题。每个新预览默认深色。
|
||||||
|
|
||||||
|
Desktop 中的鼠标点击与拖动不会选中文本,按钮之间、换行和缓冲区末尾也受保护。
|
||||||
|
这项策略只作用于 Desktop 预览,其他缓冲区和示例仍保留正常的鼠标行为。
|
||||||
|
|
||||||
|
横向按像素移动,纵向遵循 Ebox 的文本行网格。推荐至少 40 列、20 行,更大的预览
|
||||||
|
有更多摆放空间。所有文本保持当前字体大小,不使用额外字体或图片依赖。
|
||||||
|
|
||||||
|
运行 `make desktop-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`
|
||||||
|
检查真实 keymap 命令、中间运动帧、松手、遮挡归属、失败回滚和渲染一致性;
|
||||||
|
`make check` 已包含该套件。重复位置发布的耗时使用共享 interaction evaluator,
|
||||||
|
传入该示例、窗口入口 `"01 ST"` 和按键 `"M-<right>"`,不将其冒充 GUI 重绘延迟。
|
||||||
|
|
||||||
|
## 滚动性能验证
|
||||||
|
|
||||||
|
使用共享 `scripts/ebox-playground-flex-resize-evaluator.el` 的入口:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
emacs -Q --batch -L . -L ../ebox -L ../ecss -L ../tp \
|
||||||
|
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
||||||
|
-l scripts/ebox-playground-flex-resize-evaluator.el \
|
||||||
|
--eval '(ebox-playground-scroll-evaluator-run "examples/flex-reference.ebox" 104 6 36)'
|
||||||
|
```
|
||||||
|
|
||||||
|
参数依次为文件路径或路径列表、视口像素宽度、采样次数、视口行高及可选 options
|
||||||
|
plist。默认完全缓存根滚动内容,再测量顶部和中间位置的往返单行滚动。传入
|
||||||
|
`(:region-id "log")` 可选择声明了 `:id "log"` 的嵌套滚动框;`(:cold t)` 则从
|
||||||
|
初始渲染开始连续向前滚动,不预热、不主动物化全部内容,也不跳到中间位置。
|
||||||
|
两者可以组合,所选区域必须有足够的滚动空间完成采样。
|
||||||
|
|
||||||
|
初始渲染可能已包含前瞻缓存行;要测量缓存未命中,采样次数应超过该前缀,并检查
|
||||||
|
`:prefix-renders`(实际前缀渲染)和 `:materializations`(完整内容生产)计数。
|
||||||
|
已完成缓存的空操作不计入完整物化次数。
|
||||||
|
|
||||||
|
结果报告 mean/p95/max 毫秒、采样循环的 GC 次数/耗时、完整根渲染、通用文档准备、surface 计划构造、样式计算及缓存生产次数,并与独立渲染已提交输入、
|
||||||
|
恢复其滚动位置后的结果比较。比较保留 callback 和 keymap 身份,不重新加载业务
|
||||||
|
状态;移动未完成或绘制不一致都会失败。测试使用临时 buffer,并清理挂载、advice
|
||||||
|
和定时器,不改变现有预览。测量包含 Ebox/TP 发布,不包含 GUI 输入传递、原生窗口
|
||||||
|
滚动或 Emacs redisplay。此入口取代临时的逐示例滚动探针。
|
||||||
|
|
||||||
|
## 图层与原生菜单示例
|
||||||
|
|
||||||
|
打开 `examples/layer-reference.ebox` 并按 `C-c C-c`,可查看 ORBIT 空间桌面、
|
||||||
|
保留内容的重叠面板及锚定菜单;`examples/layer-minimal.ebox` 保留最小重叠示例。
|
||||||
|
工具栏每个按钮支持点击、`RET` 和 `SPC`。悬停绘制仅覆盖按钮及其自身 padding,
|
||||||
|
按钮间隙和行内剩余空间保持不变;被覆盖笔记、根菜单选项等有意设置为块级交互的
|
||||||
|
区域仍保留自己的悬停绘制。
|
||||||
|
|
||||||
|
`layer-reference.ebox` 保存布局,`layer-reference.el` 保存业务状态与操作。每个
|
||||||
|
操作通过公共 `ebox-call-with-update-batch` 将 region 变化与状态栏更新合并。
|
||||||
|
helper 加入已有 batch 时不 flush;自己拥有 batch 时只发布一次待处理变化。
|
||||||
|
回滚覆盖排队的 Ebox 更新,不撤销 companion 中任意业务状态变化,详见
|
||||||
|
[批量更新契约](../ebox/docs/user/ebox-api-reference.zh.md#合并-region-更新)。
|
||||||
|
|
||||||
|
运行 `make layer-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`,
|
||||||
|
验证图层契约、实际原生命令及已提交快照的独立渲染一致性,无需打开 GUI 窗口。
|
||||||
|
|
||||||
|
使用共享入口测量桌面展开/收起路径:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
emacs -Q --batch -L . -L ../ebox -L ../ecss -L ../tp \
|
||||||
|
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
|
||||||
|
-l scripts/ebox-playground-flex-resize-evaluator.el \
|
||||||
|
--eval '(let ((r (ebox-playground-interaction-evaluator-run "examples/layer-reference.ebox" "EXPLODE STACK" "RET" 6 104 100))) (cl-remf r :text) (pp r))'
|
||||||
|
```
|
||||||
|
|
||||||
|
根菜单关闭时,桌面三个工具栏命令只将受影响的叠层宿主和状态文字一起发布,不再
|
||||||
|
完整渲染候选树。宿主内部仍会重新合成堆叠组;这是 owner 级增量,还不是像素级
|
||||||
|
最小脏矩形。可见根浮层和依赖内容的几何仍保留扩大合成范围的回退。回归测试要求
|
||||||
|
每次命令只发布一次,并禁止完整候选渲染;耗时是诊断数据,不作为 CI 时间阈值。
|
||||||
|
|
||||||
|
<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 提取(包含局部切片)和节点树注册的调用次数。
|
||||||
|
局部补丁也会执行规划;`:full-surface-renders` 单独统计完整候选渲染次数。
|
||||||
|
`: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
|
```elisp
|
||||||
(add-to-list 'auto-mode-alist '("\\.ebox\\'" . ebox-dsl-mode))
|
(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/` 下。例如:
|
参考 fixture 直接位于 `examples/` 下。例如:
|
||||||
|
|
||||||
```elisp
|
```elisp
|
||||||
|
|||||||
@ -35,10 +35,11 @@
|
|||||||
"Header line shown by `ebox-dsl-mode'.")
|
"Header line shown by `ebox-dsl-mode'.")
|
||||||
|
|
||||||
(defun ebox-playground--source-form ()
|
(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--evaluate-form
|
||||||
(ebox-playground--read-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
|
;;;###autoload
|
||||||
(define-derived-mode ebox-dsl-mode emacs-lisp-mode "EboxDSL"
|
(define-derived-mode ebox-dsl-mode emacs-lisp-mode "EboxDSL"
|
||||||
@ -90,7 +91,7 @@
|
|||||||
"Directory containing the Ebox Playground package.")
|
"Directory containing the Ebox Playground package.")
|
||||||
|
|
||||||
(defcustom ebox-playground-default-file
|
(defcustom ebox-playground-default-file
|
||||||
(expand-file-name "examples/public-layout-gallery.ebox"
|
(expand-file-name "examples/grid-reference.ebox"
|
||||||
ebox-playground-directory)
|
ebox-playground-directory)
|
||||||
"Ebox DSL file rendered by `ebox-playground-open'."
|
"Ebox DSL file rendered by `ebox-playground-open'."
|
||||||
:type 'file
|
:type 'file
|
||||||
@ -114,12 +115,9 @@
|
|||||||
((ebox-playground--window-viewport-width window))
|
((ebox-playground--window-viewport-width window))
|
||||||
(t ebox-playground-default-viewport-width)))
|
(t ebox-playground-default-viewport-width)))
|
||||||
|
|
||||||
(defun ebox-playground--apply-preview-display-settings (&optional canvas-background)
|
(defun ebox-playground--apply-preview-display-settings ()
|
||||||
"Apply canvas redisplay settings to the current preview buffer.
|
"Apply text redisplay settings without overriding buffer chrome or faces."
|
||||||
CANVAS-BACKGROUND fills display space outside generated root lines."
|
|
||||||
(setq-local truncate-lines t)
|
(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 auto-hscroll-mode t)
|
||||||
(setq-local fringe-indicator-alist
|
(setq-local fringe-indicator-alist
|
||||||
(cl-remove-if
|
(cl-remove-if
|
||||||
@ -128,15 +126,12 @@ CANVAS-BACKGROUND fills display space outside generated root lines."
|
|||||||
(copy-tree fringe-indicator-alist)))
|
(copy-tree fringe-indicator-alist)))
|
||||||
(setq-local bidi-display-reordering nil)
|
(setq-local bidi-display-reordering nil)
|
||||||
(setq-local bidi-paragraph-direction 'left-to-right)
|
(setq-local bidi-paragraph-direction 'left-to-right)
|
||||||
(setq-local bidi-inhibit-bpa t)
|
(setq-local bidi-inhibit-bpa t))
|
||||||
(setq-local face-remapping-alist
|
|
||||||
(when (stringp canvas-background)
|
|
||||||
(list (list 'default (list :background canvas-background))))))
|
|
||||||
|
|
||||||
(defconst ebox-playground--preview-display-variables
|
(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
|
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.")
|
"Buffer-local display variables owned by a playground preview.")
|
||||||
|
|
||||||
(defun ebox-playground--preview-display-snapshot ()
|
(defun ebox-playground--preview-display-snapshot ()
|
||||||
@ -168,9 +163,7 @@ CANVAS-BACKGROUND fills display space outside generated root lines."
|
|||||||
(unwind-protect
|
(unwind-protect
|
||||||
(prog1
|
(prog1
|
||||||
(progn
|
(progn
|
||||||
(ebox-playground--apply-preview-display-settings
|
(ebox-playground--apply-preview-display-settings)
|
||||||
(ebox-style-node-specified-value
|
|
||||||
input :background-color))
|
|
||||||
(ebox-render-to-buffer buffer input))
|
(ebox-render-to-buffer buffer input))
|
||||||
(setq published-p t))
|
(setq published-p t))
|
||||||
(unless published-p
|
(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--effective-viewport-width))))
|
||||||
(ebox-playground--render-to-preview-buffer buffer input)))
|
(ebox-playground--render-to-preview-buffer buffer input)))
|
||||||
|
|
||||||
(defun ebox-playground--static-value-p (value)
|
(defun ebox-playground--evaluate-form (form &optional source-file)
|
||||||
"Return non-nil when VALUE is a literal or explicitly quoted form."
|
"Evaluate one Elisp FORM lexically to produce Ebox DSL data.
|
||||||
(or (null value) (numberp value) (stringp value) (characterp value)
|
When SOURCE-FILE names a .ebox file, load its optional same-basename .el
|
||||||
(keywordp value) (eq value t)
|
companion first. Reload that exact source file on every evaluation."
|
||||||
(and (consp value) (memq (car value) '(quote function)))))
|
(when (and source-file
|
||||||
|
(equal (file-name-extension source-file) "ebox"))
|
||||||
(defun ebox-playground--evaluate-value (value)
|
(let ((companion (concat (file-name-sans-extension
|
||||||
"Evaluate executable DSL VALUE while preserving literal data values."
|
(expand-file-name source-file)) ".el")))
|
||||||
(if (ebox-playground--static-value-p value)
|
(when (file-exists-p companion)
|
||||||
(if (and (consp value) (memq (car value) '(quote function)))
|
(load companion nil t t))))
|
||||||
(eval value nil)
|
(eval form t))
|
||||||
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--read-form (source)
|
(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))
|
(when (string-empty-p (string-trim source))
|
||||||
(user-error "The Ebox DSL source is empty"))
|
(user-error "The Ebox DSL source is empty"))
|
||||||
(with-temp-buffer
|
(with-temp-buffer
|
||||||
|
(set-syntax-table emacs-lisp-mode-syntax-table)
|
||||||
(insert source)
|
(insert source)
|
||||||
(goto-char (point-min))
|
(goto-char (point-min))
|
||||||
(let ((read-eval nil)
|
(let* ((read-eval nil)
|
||||||
(form (read (current-buffer))))
|
(form (read (current-buffer))))
|
||||||
(condition-case nil
|
(forward-comment (point-max))
|
||||||
(progn
|
(unless (eobp)
|
||||||
(read (current-buffer))
|
(user-error "The Ebox source must contain one Elisp expression"))
|
||||||
(user-error "The Ebox DSL source must contain one form"))
|
form)))
|
||||||
(end-of-file form)))))
|
|
||||||
|
|
||||||
(defun ebox-playground--read-file (file)
|
(defun ebox-playground--read-file (file)
|
||||||
"Read one Ebox DSL form from FILE."
|
"Read one Elisp expression from FILE."
|
||||||
(unless (file-readable-p file)
|
(unless (file-readable-p file)
|
||||||
(user-error "Ebox DSL file is not readable: %s" file))
|
(user-error "Ebox DSL file is not readable: %s" file))
|
||||||
(with-temp-buffer
|
(with-temp-buffer
|
||||||
@ -244,13 +215,14 @@ Visible mounted buffers follow their window through Ebox's viewport controller."
|
|||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun ebox-playground-view (&optional file viewport-width)
|
(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
|
(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-build
|
||||||
(ebox-playground--evaluate-form
|
(ebox-playground--evaluate-form
|
||||||
(ebox-playground--read-file
|
(ebox-playground--read-file file) file))))
|
||||||
(expand-file-name (or file ebox-playground-default-file)))))))
|
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun ebox-playground-open (&optional buffer-name)
|
(defun ebox-playground-open (&optional buffer-name)
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
# Ebox Playground examples
|
|
||||||
|
|
||||||
The `.ebox` files in this directory are the concrete layouts consumed by the generic `ebox-playground` runner. They are intentionally ordinary, reader-valid Ebox DSL forms so authors can copy, edit, and open them directly.
|
|
||||||
|
|
||||||
The file itself is the data form: structural forms stay unquoted, while literal list and symbol property values use quote just as they do in `etaf-view` (for example, `:gap '(1 (16))` and `:justify-content 'center`). An unquoted property value is an executable Elisp expression and is evaluated by the runner. The reader itself keeps read-time evaluation disabled.
|
|
||||||
|
|
||||||
`public-layout-gallery.ebox` is the default fixture used by `M-x ebox-playground-open`. The migrated low-level references live directly beside it: `basic.ebox`, `comprehensive.ebox`, `flex-reference.ebox`, `responsive-reference.ebox`, and `grid-reference.ebox`. The Flex reference owns its document viewport with a root `:overflow 'scroll` box: keyboard line scrolling follows the cursor's innermost scroll owner, mouse wheel scrolling follows the pointer, and residual movement bubbles through nested owners before native buffer scrolling. It keeps its original complete-content semantics during splitter resize. The Grid reference is the complete public property/value gallery: track templates (`auto`, `NFR`, `(fr FACTOR)`, `minmax`, and `repeat`), implicit tracks, row/column auto-flow, gap shorthand/longhands/aliases, placement and spans, and every supported item/content alignment value. It also documents that Ebox supports `row` and `column` auto-flow rather than browser `dense` variants. To render another fixture, visit it and press `C-c C-c`, or call `ebox-playground-open-file`.
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
# Ebox Playground 示例
|
|
||||||
|
|
||||||
此目录下的 `.ebox` 文件是通用 `ebox-playground` 运行器读取的具体布局。它们保持为普通、可读、可被 reader 读取的 Ebox DSL form,便于直接复制、编辑和打开。
|
|
||||||
|
|
||||||
文件本身就是 data form:结构表单保持不加 quote;list 和 symbol 类型的字面量属性值要和 `etaf-view` 一样加 quote,例如 `:gap '(1 (16))`、`:justify-content 'center`。未加 quote 的属性值会被运行器当作可执行的 Elisp 表达式求值;读取器本身仍然关闭 reader 求值。
|
|
||||||
|
|
||||||
`public-layout-gallery.ebox` 是 `M-x ebox-playground-open` 使用的默认 fixture。迁移来的低层参考和它并列放在 `examples/` 下:`basic.ebox`、`comprehensive.ebox`、`flex-reference.ebox`、`responsive-reference.ebox` 和 `grid-reference.ebox`。Flex 参考由根部 `:overflow 'scroll` 盒子拥有文档 viewport:键盘逐行作用于光标所在的最内层滚动 owner,鼠标滚轮按指针位置路由,剩余滚动量沿嵌套 owner 向外冒泡,最后才交给普通 buffer;splitter resize 时仍保持原有的完整内容语义。Grid 参考是完整的公共属性/值画廊:轨道模板(`auto`、`NFR`、`(fr FACTOR)`、`minmax`、`repeat`)、隐式轨道、row/column auto-flow、gap 简写/长写/别名、placement 与 span,以及所有受支持的 item/content 对齐值;同时注明 Ebox 支持 `row` 和 `column` auto-flow,不支持浏览器的 `dense` 变体。要渲染其他 fixture,可以访问该文件后按 `C-c C-c`,或调用 `ebox-playground-open-file`。
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
(box :box-sizing 'border-box :padding '(1 (12)) :border "#2563EB"
|
|
||||||
:bgcolor "#EFF6FF" :color "#1E3A8A"
|
|
||||||
"Playground file")
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
(column
|
|
||||||
(box :padding '(1 (16)) :border "#334155" :bgcolor "#F8FAFC" :color
|
|
||||||
"#0F172A" :text-align 'center
|
|
||||||
"Ebox comprehensive DSL showcase\nThe outer sections omit :width so Playground viewport sizing drives the layout.")
|
|
||||||
(box :width '(viewport) :height 1 :bgcolor "#F8FAFC")
|
|
||||||
(box :padding '(0 (14)) :border "#64748B" :bgcolor "#F8FAFC" :color
|
|
||||||
"#111827" :wrap-mode 'word
|
|
||||||
"Width rule in this file\nContainer sections use auto width. Fixed widths remain only where the example demonstrates exact item sizing, basis, min-width, or max-width.")
|
|
||||||
(box :width '(viewport) :height 1 :bgcolor "#F8FAFC")
|
|
||||||
(flex :margin '(1 0 0 1) :padding '(1 (8)) :border "#475569" :bgcolor
|
|
||||||
"#F8FAFC" :flex-flow '(row wrap) :justify-content
|
|
||||||
'space-between :align-items 'center :align-content 'flex-start
|
|
||||||
:row-gap 1 :column-gap '(12)
|
|
||||||
(box :order -1 :flex-basis '(270) :align-self 'flex-start
|
|
||||||
:width '(270) :padding '(0 (8)) :border "#BE185D"
|
|
||||||
:bgcolor "#FCE7F3" :color "#831843" :wrap-mode 'word
|
|
||||||
"Box order -1\n:flex-basis 270\nalign-self start")
|
|
||||||
(box :flex '(1 1 (220)) :width '(220) :height 4 :padding
|
|
||||||
'(0 (8)) :border "#1D4ED8" :bgcolor "#DBEAFE" :color
|
|
||||||
"#111827" :wrap-mode 'word
|
|
||||||
"Box :flex (1 1 220)\ngrow shrink basis")
|
|
||||||
(box :flex-grow 2 :flex-shrink 1 :flex-basis '(240) :align-self
|
|
||||||
'center :width '(240) :height 4 :padding '(0 (8)) :border
|
|
||||||
"#B45309" :bgcolor "#FEF3C7" :color "#78350F" :wrap-mode
|
|
||||||
'word "Box grow 2\nshrink 1\nbasis 240")
|
|
||||||
(box :flex-shrink 3 :flex-basis '(190) :align-self 'flex-end
|
|
||||||
:width '(190) :height 4 :padding '(0 (8)) :border
|
|
||||||
"#15803D" :bgcolor "#DCFCE7" :color "#14532D" :wrap-mode
|
|
||||||
'word "Box shrink 3\nbasis 190\nalign-self flex-end")))
|
|
||||||
259
examples/desktop-reference.ebox
Normal file
259
examples/desktop-reference.ebox
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
;; Layout and initial geometry live here; native gestures live in the companion.
|
||||||
|
(let* ((styles
|
||||||
|
'((desktop-root (:background-color "#0B1020" :color "#CFD8EA")
|
||||||
|
(:background-color "#F3F0E8" :color "#263347"))
|
||||||
|
(desktop-brand (:color "#BBA9FF") (:color "#674795"))
|
||||||
|
(desktop-heading (:color "#F1F5FF") (:color "#18263B"))
|
||||||
|
(desktop-muted (:color "#8F9FBD") (:color "#53637A"))
|
||||||
|
(desktop-quiet (:color "#657899") (:color "#596A84"))
|
||||||
|
(desktop-control (:background-color "#25324B" :color "#D7E3F8"
|
||||||
|
:hover-style (:background-color "#354662"))
|
||||||
|
(:background-color "#CBD6E4" :color "#253247"
|
||||||
|
:hover-style (:background-color "#B9C9DE")))
|
||||||
|
(desktop-chrome (:background-color "#25324B" :color "#C1CBE0")
|
||||||
|
(:background-color "#D5DDE8" :color "#344359"))
|
||||||
|
(desktop-workspace (:background-color "#111A2C")
|
||||||
|
(:background-color "#E3E8EF"))
|
||||||
|
(desktop-grid-label (:color "#3F5273") (:color "#566B86"))
|
||||||
|
(desktop-grid-dots (:color "#334866") (:color "#A6B3C3"))
|
||||||
|
(desktop-grid-hint (:color "#5A6D8E") (:color "#566A83"))
|
||||||
|
(desktop-studio (:background-color "#26223B" :color "#D6CDEF")
|
||||||
|
(:background-color "#E9E3F2" :color "#4F3F67"))
|
||||||
|
(desktop-studio-heading (:color "#F5F0FF") (:color "#37284F"))
|
||||||
|
(desktop-studio-caption (:color "#A999C8") (:color "#67577E"))
|
||||||
|
(desktop-studio-text (:color "#B8A9D0") (:color "#5F4D78"))
|
||||||
|
(desktop-studio-muted (:color "#85799F") (:color "#6D587F"))
|
||||||
|
(desktop-art-form (:background-color "#7561AC" :color "#EAE1FF")
|
||||||
|
(:background-color "#C9BCE3" :color "#453259"))
|
||||||
|
(desktop-art-space (:background-color "#BBA9FF" :color "#302449")
|
||||||
|
(:background-color "#DED3F6" :color "#51356C"))
|
||||||
|
(desktop-art-play (:background-color "#D8EAAB" :color "#303D22")
|
||||||
|
(:background-color "#DBE8C3" :color "#41512B"))
|
||||||
|
(desktop-notes (:background-color "#EEE3CB" :color "#514430")
|
||||||
|
(:background-color "#FFF4D8" :color "#5B482B"))
|
||||||
|
(desktop-notes-caption (:color "#8C744E") (:color "#7A623E"))
|
||||||
|
(desktop-notes-muted (:color "#887651") (:color "#836D49"))
|
||||||
|
(desktop-notes-rule (:color "#A18B61") (:color "#A38E65"))
|
||||||
|
(desktop-notes-footer (:color "#746345") (:color "#705935"))
|
||||||
|
(desktop-signal (:background-color "#17343E" :color "#BFE5E9")
|
||||||
|
(:background-color "#DBEEF0" :color "#315B65"))
|
||||||
|
(desktop-signal-heading (:color "#E4FBF9") (:color "#184B57"))
|
||||||
|
(desktop-signal-caption (:color "#7BAEB9") (:color "#436971"))
|
||||||
|
(desktop-signal-muted (:color "#699CA9") (:color "#496F77"))
|
||||||
|
(desktop-signal-accent (:color "#8DE1E4") (:color "#2E7280"))
|
||||||
|
(desktop-wave-muted (:background-color "#518F9D") (:background-color "#9EC9CF"))
|
||||||
|
(desktop-wave-cyan (:background-color "#8DE1E4") (:background-color "#5EADB5"))
|
||||||
|
(desktop-wave-violet (:background-color "#BBA9FF") (:background-color "#927AC9"))
|
||||||
|
(desktop-terminal (:background-color "#101F22" :color "#B6D7CC")
|
||||||
|
(:background-color "#E0EEE4" :color "#355E4C"))
|
||||||
|
(desktop-terminal-accent (:color "#A7E8BB") (:color "#256544"))
|
||||||
|
(desktop-terminal-muted (:color "#608C7F") (:color "#496D5B"))
|
||||||
|
(desktop-palette (:background-color "#39283D" :color "#EBCBDE")
|
||||||
|
(:background-color "#F0E1EB" :color "#65485E"))
|
||||||
|
(desktop-palette-caption (:color "#AF8DA9") (:color "#7B5670"))
|
||||||
|
(desktop-palette-accent (:color "#F3A8C9") (:color "#925277"))
|
||||||
|
(desktop-swatch-violet (:background-color "#BBA9FF") (:background-color "#9578CF"))
|
||||||
|
(desktop-swatch-amber (:background-color "#F6C879") (:background-color "#DEB251"))
|
||||||
|
(desktop-swatch-cyan (:background-color "#8DE1E4") (:background-color "#58A7AF"))
|
||||||
|
(desktop-swatch-mint (:background-color "#A7E8BB") (:background-color "#72AB83"))
|
||||||
|
(desktop-swatch-rose (:background-color "#F3A8C9") (:background-color "#CE87AA"))))
|
||||||
|
(studio (desktop-demo-window-create
|
||||||
|
:id "desk-studio" :label "STUDIO" :accent "#BBA9FF" :ink "#241B40"
|
||||||
|
:light-accent "#6D4AA2" :light-ink "#FFFFFF"
|
||||||
|
:left '(% 5) :top '(lh 1)))
|
||||||
|
(notes (desktop-demo-window-create
|
||||||
|
:id "desk-notes" :label "NOTES" :accent "#F6C879" :ink "#3B2917"
|
||||||
|
:light-accent "#8C6421" :light-ink "#FFFFFF"
|
||||||
|
:left '(% 45) :top '(% 4)))
|
||||||
|
(signal (desktop-demo-window-create
|
||||||
|
:id "desk-signal" :label "SIGNAL" :accent "#8DE1E4" :ink "#15383D"
|
||||||
|
:light-accent "#246675" :light-ink "#FFFFFF"
|
||||||
|
:left '(% 57) :top '(% 32)))
|
||||||
|
(terminal (desktop-demo-window-create
|
||||||
|
:id "desk-terminal" :label "TERMINAL" :accent "#A7E8BB" :ink "#1B3826"
|
||||||
|
:light-accent "#2F674B" :light-ink "#FFFFFF"
|
||||||
|
:left '(% 8) :top '(% 46)))
|
||||||
|
(palette (desktop-demo-window-create
|
||||||
|
:id "desk-palette" :label "PALETTE" :accent "#F3A8C9" :ink "#402438"
|
||||||
|
:light-accent "#875270" :light-ink "#FFFFFF"
|
||||||
|
:left '(% 43) :top '(% 65)))
|
||||||
|
(panes (list studio notes signal terminal palette))
|
||||||
|
(demo (desktop-demo-create :windows panes :styles styles
|
||||||
|
:order (mapcar #'desktop-demo-window-id panes)
|
||||||
|
:active "desk-palette")))
|
||||||
|
`(column :id "desktop-root" :class desktop-root
|
||||||
|
:width (vw 100) :height (vh 100) :overflow hidden
|
||||||
|
,@(desktop-demo-style demo 'desktop-root)
|
||||||
|
(box :height (lh 3) :padding-inline (ch 1)
|
||||||
|
(row :height (lh 1)
|
||||||
|
(box :class desktop-brand ,@(desktop-demo-style demo 'desktop-brand)
|
||||||
|
:font-weight bold "P A R A L L A X")
|
||||||
|
(box :padding-left (ch 2) :class desktop-quiet
|
||||||
|
,@(desktop-demo-style demo 'desktop-quiet) "DESKTOP / 05"))
|
||||||
|
(box :class desktop-heading ,@(desktop-demo-style demo 'desktop-heading)
|
||||||
|
"A little space. A lot of possibilities.")
|
||||||
|
(box :class desktop-muted ,@(desktop-demo-style demo 'desktop-muted)
|
||||||
|
"Click to focus. Drag a title bar to move."))
|
||||||
|
(flex :height (lh 1) :gap ((lh 0) (ch 1)) :padding-inline (ch 1)
|
||||||
|
(box :padding-inline (ch 1) :flex-shrink 0 :class desktop-control
|
||||||
|
,@(desktop-demo-style demo 'desktop-control) :pointer hand
|
||||||
|
:help-echo "Restore the five original positions and stacking order."
|
||||||
|
:keymap ,(desktop-demo-control demo 'reset) "RESET")
|
||||||
|
(box :padding-inline (ch 1) :flex-shrink 0 :class desktop-control
|
||||||
|
,@(desktop-demo-style demo 'desktop-control) :pointer hand
|
||||||
|
:help-echo "Bring the backmost window to the front."
|
||||||
|
:keymap ,(desktop-demo-control demo 'next) "CYCLE")
|
||||||
|
(box :id "desktop-theme" :class desktop-control
|
||||||
|
,@(desktop-demo-style demo 'desktop-control)
|
||||||
|
:width (ch 7) :flex-shrink 0 :text-align center :pointer hand
|
||||||
|
:help-echo "Switch the whole desktop between light and dark palettes."
|
||||||
|
:keymap ,(desktop-demo-control demo 'theme) "LIGHT")
|
||||||
|
(box :class desktop-quiet :min-width (px 0) :overflow hidden :wrap-mode none
|
||||||
|
,@(desktop-demo-style demo 'desktop-quiet)
|
||||||
|
"FIVE IDEAS / ONE SHARED SPACE"))
|
||||||
|
|
||||||
|
(box :id "desktop-canvas" :height (calc (- (vh 100) (lh 7)))
|
||||||
|
:overflow hidden :class desktop-workspace
|
||||||
|
,@(desktop-demo-style demo 'desktop-workspace)
|
||||||
|
(box :position absolute :left (ch 1) :top (lh 0)
|
||||||
|
:class desktop-grid-label ,@(desktop-demo-style demo 'desktop-grid-label)
|
||||||
|
"WORKSPACE 01 + + + + +")
|
||||||
|
(box :position absolute :left (% 68) :top (% 78)
|
||||||
|
:width (% 30) :class desktop-grid-dots ,@(desktop-demo-style demo 'desktop-grid-dots)
|
||||||
|
"+ . + . +\n. + . + .\n+ . + . +\n. + . + .")
|
||||||
|
(box :position absolute :left (ch 2) :top (% 93)
|
||||||
|
:class desktop-grid-hint ,@(desktop-demo-style demo 'desktop-grid-hint)
|
||||||
|
"MOVE THINGS AROUND. MAKE IT YOURS.")
|
||||||
|
|
||||||
|
(box :id "desk-studio" :position absolute :left ,(desktop-demo-window-left studio)
|
||||||
|
:top ,(desktop-demo-window-top studio) :width (min (ch 44) (% 55))
|
||||||
|
:height (lh 13) :z-index 1 :overflow hidden
|
||||||
|
:class desktop-studio ,@(desktop-demo-style demo 'desktop-studio) :pointer arrow
|
||||||
|
:help-echo "Studio. Click any exposed area to bring this whole window forward."
|
||||||
|
:keymap ,(desktop-demo-keymap demo "desk-studio")
|
||||||
|
(box :id "desk-studio-title" :height (lh 1) :padding-inline (ch 1)
|
||||||
|
:class desktop-chrome ,@(desktop-demo-style demo 'desktop-chrome) :font-weight bold
|
||||||
|
:pointer hand :keymap ,(desktop-demo-keymap demo "desk-studio" t)
|
||||||
|
:help-echo "Drag Studio. Alt+arrows moves it from the keyboard."
|
||||||
|
"STUDIO / 01 ::")
|
||||||
|
(box :padding ((lh 1) (ch 2))
|
||||||
|
(box :font-weight bold :class desktop-studio-heading ,@(desktop-demo-style demo 'desktop-studio-heading) "Make space for the unexpected.")
|
||||||
|
(box :padding-top (lh 1) :class desktop-studio-caption ,@(desktop-demo-style demo 'desktop-studio-caption) "EXPERIMENT 024 / SOFT STRUCTURES")
|
||||||
|
(box :height (lh 5) :padding-top (lh 1)
|
||||||
|
(box :position absolute :left (ch 0) :top (lh 1)
|
||||||
|
:width (ch 13) :height (lh 3) :class desktop-art-form ,@(desktop-demo-style demo 'desktop-art-form) :text-align center "\nFORM / 01")
|
||||||
|
(box :position absolute :left (ch 9) :top (lh 0)
|
||||||
|
:width (ch 13) :height (lh 3) :z-index 1
|
||||||
|
:class desktop-art-space ,@(desktop-demo-style demo 'desktop-art-space)
|
||||||
|
:text-align center "\nSPACE / 02")
|
||||||
|
(box :position absolute :left (ch 18) :top (lh 2)
|
||||||
|
:width (ch 12) :height (lh 3) :z-index 2
|
||||||
|
:class desktop-art-play ,@(desktop-demo-style demo 'desktop-art-play)
|
||||||
|
:text-align center "\nPLAY / 03"))
|
||||||
|
(box :class desktop-studio-text ,@(desktop-demo-style demo 'desktop-studio-text) "Fragments become a composition.")
|
||||||
|
(box :padding-top (lh 1) :class desktop-studio-muted ,@(desktop-demo-style demo 'desktop-studio-muted) "COLLECTION / UNFINISHED POSSIBILITIES")))
|
||||||
|
|
||||||
|
(box :id "desk-notes" :position absolute :left ,(desktop-demo-window-left notes)
|
||||||
|
:top ,(desktop-demo-window-top notes) :width (min (ch 34) (% 43))
|
||||||
|
:height (lh 11) :z-index 2 :overflow hidden
|
||||||
|
:class desktop-notes ,@(desktop-demo-style demo 'desktop-notes) :pointer arrow
|
||||||
|
:help-echo "Notes. The content remains here even when another window covers it."
|
||||||
|
:keymap ,(desktop-demo-keymap demo "desk-notes")
|
||||||
|
(box :id "desk-notes-title" :height (lh 1) :padding-inline (ch 1)
|
||||||
|
:class desktop-chrome ,@(desktop-demo-style demo 'desktop-chrome) :font-weight bold
|
||||||
|
:pointer hand :keymap ,(desktop-demo-keymap demo "desk-notes" t)
|
||||||
|
:help-echo "Drag Notes. Alt+arrows moves it from the keyboard."
|
||||||
|
"NOTES / 02 ::")
|
||||||
|
(box :padding ((lh 1) (ch 1))
|
||||||
|
(box :font-weight bold "A quieter kind of focus.")
|
||||||
|
(box :padding-top (lh 1) :class desktop-notes-caption ,@(desktop-demo-style demo 'desktop-notes-caption) "TODAY / THINGS WORTH MAKING")
|
||||||
|
(box :padding-top (lh 1)
|
||||||
|
"[x] Collect the small ideas\n[ ] Give the good ones room\n[ ] Leave something unfinished")
|
||||||
|
(box :padding-top (lh 1) :class desktop-notes-muted ,@(desktop-demo-style demo 'desktop-notes-muted) "Less noise. More intention.")
|
||||||
|
(box :class desktop-notes-rule ,@(desktop-demo-style demo 'desktop-notes-rule) "------------------------------")
|
||||||
|
(box :class desktop-notes-footer ,@(desktop-demo-style demo 'desktop-notes-footer) "03 NOTES / SAVED IN THE MOMENT")))
|
||||||
|
|
||||||
|
(box :id "desk-signal" :position absolute :left ,(desktop-demo-window-left signal)
|
||||||
|
:top ,(desktop-demo-window-top signal) :width (min (ch 32) (% 39))
|
||||||
|
:height (lh 10) :z-index 3 :overflow hidden
|
||||||
|
:class desktop-signal ,@(desktop-demo-style demo 'desktop-signal) :pointer arrow
|
||||||
|
:help-echo "Signal. Raise the entire player, including its overlapping waveform."
|
||||||
|
:keymap ,(desktop-demo-keymap demo "desk-signal")
|
||||||
|
(box :id "desk-signal-title" :height (lh 1) :padding-inline (ch 1)
|
||||||
|
:class desktop-chrome ,@(desktop-demo-style demo 'desktop-chrome) :font-weight bold
|
||||||
|
:pointer hand :keymap ,(desktop-demo-keymap demo "desk-signal" t)
|
||||||
|
:help-echo "Drag Signal. Alt+arrows moves it from the keyboard."
|
||||||
|
"SIGNAL / 03 ::")
|
||||||
|
(box :padding ((lh 1) (ch 2))
|
||||||
|
(box :font-weight bold :class desktop-signal-heading ,@(desktop-demo-style demo 'desktop-signal-heading) "AFTER HOURS")
|
||||||
|
(box :class desktop-signal-caption ,@(desktop-demo-style demo 'desktop-signal-caption) "A soundtrack for the in-between.")
|
||||||
|
(flex :height (lh 3) :gap ((lh 0) (ch 1)) :padding-top (lh 1)
|
||||||
|
(box :width (ch 2) :height (lh 1) :class desktop-wave-muted ,@(desktop-demo-style demo 'desktop-wave-muted))
|
||||||
|
(box :width (ch 2) :height (lh 2) :class desktop-wave-cyan ,@(desktop-demo-style demo 'desktop-wave-cyan))
|
||||||
|
(box :width (ch 2) :height (lh 1) :class desktop-wave-muted ,@(desktop-demo-style demo 'desktop-wave-muted))
|
||||||
|
(box :width (ch 2) :height (lh 2) :class desktop-wave-violet ,@(desktop-demo-style demo 'desktop-wave-violet))
|
||||||
|
(box :width (ch 2) :height (lh 1) :class desktop-wave-cyan ,@(desktop-demo-style demo 'desktop-wave-cyan))
|
||||||
|
(box :width (ch 2) :height (lh 2) :class desktop-wave-violet ,@(desktop-demo-style demo 'desktop-wave-violet))
|
||||||
|
(box :width (ch 2) :height (lh 1) :class desktop-wave-muted ,@(desktop-demo-style demo 'desktop-wave-muted)))
|
||||||
|
(box :class desktop-signal-accent ,@(desktop-demo-style demo 'desktop-signal-accent) "========------------ 03:42")
|
||||||
|
(box :padding-top (lh 1) :class desktop-signal-muted ,@(desktop-demo-style demo 'desktop-signal-muted) "SIDE B / SLOWER ORBITS")))
|
||||||
|
|
||||||
|
(box :id "desk-terminal" :position absolute :left ,(desktop-demo-window-left terminal)
|
||||||
|
:top ,(desktop-demo-window-top terminal) :width (min (ch 48) (% 60))
|
||||||
|
:height (lh 11) :z-index 4 :overflow hidden
|
||||||
|
:class desktop-terminal ,@(desktop-demo-style demo 'desktop-terminal) :pointer arrow
|
||||||
|
:help-echo "Terminal. This sample transcript is part of the movable text window."
|
||||||
|
:keymap ,(desktop-demo-keymap demo "desk-terminal")
|
||||||
|
(box :id "desk-terminal-title" :height (lh 1) :padding-inline (ch 1)
|
||||||
|
:class desktop-chrome ,@(desktop-demo-style demo 'desktop-chrome) :font-weight bold
|
||||||
|
:pointer hand :keymap ,(desktop-demo-keymap demo "desk-terminal" t)
|
||||||
|
:help-echo "Drag Terminal. Alt+arrows moves it from the keyboard."
|
||||||
|
"TERMINAL / 04 ::")
|
||||||
|
(box :padding ((lh 1) (ch 2))
|
||||||
|
(box :class desktop-terminal-accent ,@(desktop-demo-style demo 'desktop-terminal-accent) "$ build something good")
|
||||||
|
(box :padding-top (lh 1) :class desktop-terminal-muted ,@(desktop-demo-style demo 'desktop-terminal-muted) " session: creative / local\n workspace: parallax")
|
||||||
|
(box :padding-top (lh 1) " + ideas connected\n + room for experiments\n + all five windows ready")
|
||||||
|
(box :padding-top (lh 1) :class desktop-terminal-accent ,@(desktop-demo-style demo 'desktop-terminal-accent) " READY / follow your curiosity")))
|
||||||
|
|
||||||
|
(box :id "desk-palette" :position absolute :left ,(desktop-demo-window-left palette)
|
||||||
|
:top ,(desktop-demo-window-top palette) :width (min (ch 29) (% 36))
|
||||||
|
:height (lh 9) :z-index 5 :overflow hidden
|
||||||
|
:class desktop-palette ,@(desktop-demo-style demo 'desktop-palette) :pointer arrow
|
||||||
|
:help-echo "Palette starts in front. Click a window underneath to change the stack."
|
||||||
|
:keymap ,(desktop-demo-keymap demo "desk-palette")
|
||||||
|
(box :id "desk-palette-title" :height (lh 1) :padding-inline (ch 1)
|
||||||
|
:class desktop-chrome ,@(desktop-demo-chrome-style demo palette t) :font-weight bold
|
||||||
|
:pointer hand :keymap ,(desktop-demo-keymap demo "desk-palette" t)
|
||||||
|
:help-echo "Drag Palette. Alt+arrows moves it from the keyboard."
|
||||||
|
"PALETTE / 05 ::")
|
||||||
|
(box :padding ((lh 1) (ch 1))
|
||||||
|
(box :font-weight bold "Objects in conversation.")
|
||||||
|
(box :padding-top (lh 1) :class desktop-palette-caption ,@(desktop-demo-style demo 'desktop-palette-caption) "FIVE TONES / ONE ATMOSPHERE")
|
||||||
|
(flex :height (lh 2) :padding-top (lh 1) :gap ((lh 0) (ch 1))
|
||||||
|
(box :width (ch 4) :height (lh 1) :class desktop-swatch-violet
|
||||||
|
,@(desktop-demo-style demo 'desktop-swatch-violet))
|
||||||
|
(box :width (ch 4) :height (lh 1) :class desktop-swatch-amber
|
||||||
|
,@(desktop-demo-style demo 'desktop-swatch-amber))
|
||||||
|
(box :width (ch 4) :height (lh 1) :class desktop-swatch-cyan
|
||||||
|
,@(desktop-demo-style demo 'desktop-swatch-cyan))
|
||||||
|
(box :width (ch 4) :height (lh 1) :class desktop-swatch-mint
|
||||||
|
,@(desktop-demo-style demo 'desktop-swatch-mint))
|
||||||
|
(box :width (ch 4) :height (lh 1) :class desktop-swatch-rose
|
||||||
|
,@(desktop-demo-style demo 'desktop-swatch-rose)))
|
||||||
|
(box :padding-top (lh 1) :class desktop-palette-accent ,@(desktop-demo-style demo 'desktop-palette-accent) "MIDNIGHT / SOFT ELECTRIC"))))
|
||||||
|
|
||||||
|
(flex :height (lh 2) :gap ((lh 0) (ch 1)) :padding-top (lh 1)
|
||||||
|
,@(cl-loop for pane in panes for index from 1
|
||||||
|
for short in '("ST" "NT" "SG" "TM" "PL")
|
||||||
|
for active = (equal (desktop-demo-window-id pane) (desktop-demo-active demo))
|
||||||
|
collect
|
||||||
|
`(box :id ,(concat (desktop-demo-window-id pane) "-dock")
|
||||||
|
:padding-inline (ch 1)
|
||||||
|
:class desktop-chrome ,@(desktop-demo-chrome-style demo pane active)
|
||||||
|
:font-weight bold :pointer hand
|
||||||
|
:help-echo ,(format "Bring %s to front, even when covered. Alt+arrows moves it."
|
||||||
|
(desktop-demo-window-label pane))
|
||||||
|
:keymap ,(desktop-demo-keymap demo (desktop-demo-window-id pane))
|
||||||
|
,(format "%02d %s" index short))))
|
||||||
|
(box :id "desktop-status" :height (lh 1) :class desktop-muted ,@(desktop-demo-style demo 'desktop-muted)
|
||||||
|
,(desktop-demo-status demo))))
|
||||||
318
examples/desktop-reference.el
Normal file
318
examples/desktop-reference.el
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
;;; desktop-reference.el --- Direct manipulation for the desktop example -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; The matching .ebox file declares the five windows and their initial layout.
|
||||||
|
;; Native mouse tracking publishes positions while the button is held. All
|
||||||
|
;; document changes use public Ebox region updates; each preview owns its state.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ebox)
|
||||||
|
(require 'ebox-selector)
|
||||||
|
|
||||||
|
(defun desktop-demo--dismiss-mouse-selection ()
|
||||||
|
"Consume a native text-selection command in a desktop preview."
|
||||||
|
(interactive)
|
||||||
|
(deactivate-mark))
|
||||||
|
|
||||||
|
(defvar desktop-demo-input-mode-map
|
||||||
|
(let ((map (make-sparse-keymap)))
|
||||||
|
(dolist (command '(mouse-drag-region mouse-set-region mouse-set-point
|
||||||
|
mouse-save-then-kill mouse-drag-secondary
|
||||||
|
mouse-start-secondary mouse-set-secondary
|
||||||
|
mouse-secondary-save-then-kill))
|
||||||
|
(define-key map (vector 'remap command) #'desktop-demo--dismiss-mouse-selection))
|
||||||
|
map)
|
||||||
|
"Desktop-local fallbacks for mouse events outside individual controls.")
|
||||||
|
|
||||||
|
(defun desktop-demo--clear-mouse-mark ()
|
||||||
|
"Clear a preceding text selection when a desktop mouse command starts."
|
||||||
|
(when (mouse-event-p last-input-event)
|
||||||
|
(deactivate-mark)))
|
||||||
|
|
||||||
|
(define-minor-mode desktop-demo-input-mode
|
||||||
|
"Keep desktop gestures from entering native text selection.
|
||||||
|
Control text keymaps retain their own activation and title-drag commands."
|
||||||
|
:lighter nil :keymap desktop-demo-input-mode-map
|
||||||
|
(if desktop-demo-input-mode
|
||||||
|
(progn
|
||||||
|
(deactivate-mark)
|
||||||
|
(add-hook 'pre-command-hook #'desktop-demo--clear-mouse-mark nil t))
|
||||||
|
(remove-hook 'pre-command-hook #'desktop-demo--clear-mouse-mark t)))
|
||||||
|
|
||||||
|
(defun desktop-demo--sync-input-mode ()
|
||||||
|
"Apply the desktop mouse policy only to a successfully mounted desktop."
|
||||||
|
(desktop-demo-input-mode
|
||||||
|
(if (and ebox-buffer-mode
|
||||||
|
(condition-case nil
|
||||||
|
(ebox-region-resolve (current-buffer) "desktop-root")
|
||||||
|
(user-error nil)))
|
||||||
|
1 -1)))
|
||||||
|
|
||||||
|
(add-hook 'ebox-buffer-mode-hook #'desktop-demo--sync-input-mode)
|
||||||
|
|
||||||
|
(cl-defstruct (desktop-demo-window (:constructor desktop-demo-window-create))
|
||||||
|
"A desktop pane's identity, palette, initial offsets and pixel displacement."
|
||||||
|
id label accent ink light-accent light-ink left top (x 0) (y 0))
|
||||||
|
|
||||||
|
(cl-defstruct (desktop-demo (:constructor desktop-demo-create))
|
||||||
|
"Independent state captured by the native commands of one desktop preview."
|
||||||
|
windows order active styles (theme 'dark) (serial 5) dragging)
|
||||||
|
|
||||||
|
(defun desktop-demo-style (demo role)
|
||||||
|
"Return DEMO's current theme declarations for the semantic style ROLE."
|
||||||
|
(let ((entry (assq role (desktop-demo-styles demo))))
|
||||||
|
(unless entry (error "Unknown desktop style: %s" role))
|
||||||
|
(nth (if (eq (desktop-demo-theme demo) 'light) 2 1) entry)))
|
||||||
|
|
||||||
|
(defun desktop-demo-chrome-style (demo pane active)
|
||||||
|
"Return DEMO's themed title/dock declarations for PANE and ACTIVE state."
|
||||||
|
(if active
|
||||||
|
(if (eq (desktop-demo-theme demo) 'light)
|
||||||
|
(list :background-color (desktop-demo-window-light-accent pane)
|
||||||
|
:color (desktop-demo-window-light-ink pane))
|
||||||
|
(list :background-color (desktop-demo-window-accent pane)
|
||||||
|
:color (desktop-demo-window-ink pane)))
|
||||||
|
(desktop-demo-style demo 'desktop-chrome)))
|
||||||
|
|
||||||
|
(defun desktop-demo--window (demo id)
|
||||||
|
"Return DEMO's pane with semantic ID."
|
||||||
|
(or (cl-find id (desktop-demo-windows demo)
|
||||||
|
:key #'desktop-demo-window-id :test #'equal)
|
||||||
|
(error "Unknown desktop window: %s" id)))
|
||||||
|
|
||||||
|
(defun desktop-demo-status (demo)
|
||||||
|
"Describe DEMO's frontmost pane and its displacement."
|
||||||
|
(let ((pane (desktop-demo--window demo (desktop-demo-active demo))))
|
||||||
|
(format " %s / FRONT X %+d Y %+d | DRAG A TITLE BAR"
|
||||||
|
(desktop-demo-window-label pane)
|
||||||
|
(desktop-demo-window-x pane) (desktop-demo-window-y pane))))
|
||||||
|
|
||||||
|
(defun desktop-demo--chrome (demo pane active)
|
||||||
|
"Return DEMO's themed title and dock updates for PANE and ACTIVE state."
|
||||||
|
(let ((style (desktop-demo-chrome-style demo pane active)))
|
||||||
|
(mapcar (lambda (suffix)
|
||||||
|
(cons (concat (desktop-demo-window-id pane) suffix) style))
|
||||||
|
'("-title" "-dock"))))
|
||||||
|
|
||||||
|
(defun desktop-demo--keyboard-anchor ()
|
||||||
|
"Capture the native control at point for keyboard repeat, if applicable."
|
||||||
|
(unless (mouse-event-p last-input-event)
|
||||||
|
(let* ((map (get-text-property (point) 'keymap))
|
||||||
|
(command (and (keymapp map) (lookup-key map [mouse-1]))))
|
||||||
|
(when (commandp command)
|
||||||
|
(list command
|
||||||
|
(- (point) (previous-single-property-change
|
||||||
|
(1+ (point)) 'keymap nil (point-min))))))))
|
||||||
|
|
||||||
|
(defun desktop-demo--restore-keyboard-anchor (anchor)
|
||||||
|
"Restore point within ANCHOR's surviving native control after publication."
|
||||||
|
(when anchor
|
||||||
|
(let ((position (point-min)))
|
||||||
|
(catch 'restored
|
||||||
|
(while (< position (point-max))
|
||||||
|
(let ((map (get-text-property position 'keymap))
|
||||||
|
(end (next-single-property-change position 'keymap nil (point-max))))
|
||||||
|
(when (and (keymapp map) (eq (lookup-key map [mouse-1]) (car anchor)))
|
||||||
|
(goto-char (+ position (min (cadr anchor) (1- (- end position)))))
|
||||||
|
(throw 'restored t))
|
||||||
|
(setq position end)))))))
|
||||||
|
|
||||||
|
(defun desktop-demo--publish (demo candidate updates)
|
||||||
|
"Publish UPDATES and CANDIDATE's status, then advance DEMO's state.
|
||||||
|
Failed Ebox publication leaves the business state on its accepted generation."
|
||||||
|
(let ((anchor (desktop-demo--keyboard-anchor)))
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(dolist (update updates)
|
||||||
|
(apply #'ebox-region-update update))
|
||||||
|
(ebox-region-update "desktop-status" :content (desktop-demo-status candidate))))
|
||||||
|
(setf (desktop-demo-windows demo) (desktop-demo-windows candidate)
|
||||||
|
(desktop-demo-order demo) (desktop-demo-order candidate)
|
||||||
|
(desktop-demo-active demo) (desktop-demo-active candidate)
|
||||||
|
(desktop-demo-theme demo) (desktop-demo-theme candidate)
|
||||||
|
(desktop-demo-serial demo) (desktop-demo-serial candidate)
|
||||||
|
(desktop-demo-dragging demo) (desktop-demo-dragging candidate))
|
||||||
|
(desktop-demo--restore-keyboard-anchor anchor)))
|
||||||
|
|
||||||
|
(defun desktop-demo-activate (demo id &optional dragging)
|
||||||
|
"Bring DEMO's pane ID forward; DRAGGING begins a transient mouse gesture."
|
||||||
|
(let ((pane (desktop-demo--window demo id)))
|
||||||
|
(unless (equal id (desktop-demo-active demo))
|
||||||
|
(let ((candidate (copy-desktop-demo demo)))
|
||||||
|
(setf (desktop-demo-active candidate) id
|
||||||
|
(desktop-demo-order candidate)
|
||||||
|
(append (remove id (desktop-demo-order demo)) (list id))
|
||||||
|
(desktop-demo-serial candidate) (1+ (desktop-demo-serial demo)))
|
||||||
|
(desktop-demo--publish
|
||||||
|
demo candidate
|
||||||
|
(append (list (list id :z-index (desktop-demo-serial candidate)))
|
||||||
|
(desktop-demo--chrome
|
||||||
|
demo (desktop-demo--window demo (desktop-demo-active demo)) nil)
|
||||||
|
(desktop-demo--chrome demo pane t)))))
|
||||||
|
(when dragging (setf (desktop-demo-dragging demo) id))))
|
||||||
|
|
||||||
|
(defun desktop-demo-move (demo id x y &optional row-height)
|
||||||
|
"Move DEMO's pane ID to pixel displacement X, Y from its initial offsets.
|
||||||
|
ROW-HEIGHT is the drag frame's host row size, defaulting to the selected frame.
|
||||||
|
Return non-nil only after a changed position has been published."
|
||||||
|
(let ((pane (desktop-demo--window demo id)))
|
||||||
|
(unless (and (= x (desktop-demo-window-x pane))
|
||||||
|
(= y (desktop-demo-window-y pane)))
|
||||||
|
(let ((candidate (copy-desktop-demo demo))
|
||||||
|
(moved (copy-desktop-demo-window pane))
|
||||||
|
(rows (/ y (float (max 1 (or row-height (frame-char-height)))))))
|
||||||
|
(setf (desktop-demo-window-x moved) x
|
||||||
|
(desktop-demo-window-y moved) y
|
||||||
|
(desktop-demo-windows candidate)
|
||||||
|
(mapcar (lambda (item) (if (eq item pane) moved item))
|
||||||
|
(desktop-demo-windows demo)))
|
||||||
|
(desktop-demo--publish
|
||||||
|
demo candidate
|
||||||
|
(list (list id :left `(calc (+ ,(desktop-demo-window-left pane) (px ,x)))
|
||||||
|
:top `(calc (+ ,(desktop-demo-window-top pane) (lh ,rows))))))
|
||||||
|
t))))
|
||||||
|
|
||||||
|
(defun desktop-demo--frame-point (position frame)
|
||||||
|
"Convert a supported mouse POSITION to pixels in FRAME, or return nil.
|
||||||
|
Text-window positions and same-frame outside-window positions are supported;
|
||||||
|
fringe, scrollbar and other area-specific coordinate systems are skipped."
|
||||||
|
(let ((where (posn-window position)) (xy (posn-x-y position)))
|
||||||
|
(when (and (consp xy) (numberp (car xy)) (numberp (cdr xy)))
|
||||||
|
(cond
|
||||||
|
((eq where frame) xy)
|
||||||
|
((and (window-live-p where) (eq (window-frame where) frame)
|
||||||
|
(null (posn-area position)))
|
||||||
|
(let ((edges (window-inside-pixel-edges where)))
|
||||||
|
(cons (+ (car edges) (car xy)) (+ (cadr edges) (cdr xy)))))))))
|
||||||
|
|
||||||
|
(defun desktop-demo-drag (demo id event)
|
||||||
|
"Raise DEMO's pane ID and track its title-bar press EVENT until release.
|
||||||
|
Publish each changed position before reading the next event. Preserve the
|
||||||
|
grab offset, consume the release endpoint and retain unrelated input. Quit
|
||||||
|
or failure releases gesture state and keeps the last accepted geometry."
|
||||||
|
(let* ((position (event-start event))
|
||||||
|
(window (posn-window position)))
|
||||||
|
(unless (and (window-live-p window) (null (posn-area position)))
|
||||||
|
(user-error "Drag a desktop title bar in a live text window"))
|
||||||
|
(let* ((buffer (window-buffer window))
|
||||||
|
(frame (window-frame window))
|
||||||
|
(start (desktop-demo--frame-point position frame))
|
||||||
|
(pane (desktop-demo--window demo id))
|
||||||
|
(origin-x (desktop-demo-window-x pane))
|
||||||
|
(origin-y (desktop-demo-window-y pane))
|
||||||
|
(row-height (max 1 (frame-char-height frame)))
|
||||||
|
(button (event-basic-type event))
|
||||||
|
(mouse-fine-grained-tracking t))
|
||||||
|
(unless start (user-error "The drag event has no text coordinates"))
|
||||||
|
(unwind-protect
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(desktop-demo-activate demo id t)
|
||||||
|
(redisplay t)
|
||||||
|
(track-mouse
|
||||||
|
(let ((track-mouse 'dragging) done)
|
||||||
|
(while (and (not done) (window-live-p window)
|
||||||
|
(buffer-live-p buffer) (eq (window-buffer window) buffer)
|
||||||
|
(ebox-surface-buffer-mounted-p buffer))
|
||||||
|
(let* ((next (read-event))
|
||||||
|
(release
|
||||||
|
(and (eq (event-basic-type next) button)
|
||||||
|
(or (memq 'click (event-modifiers next))
|
||||||
|
(memq 'drag (event-modifiers next)))))
|
||||||
|
(point (when (or release (mouse-movement-p next))
|
||||||
|
(desktop-demo--frame-point
|
||||||
|
(if release (event-end next) (event-start next)) frame))))
|
||||||
|
(when (and point (window-live-p window) (buffer-live-p buffer)
|
||||||
|
(eq (window-buffer window) buffer)
|
||||||
|
(ebox-surface-buffer-mounted-p buffer))
|
||||||
|
(let* ((edges (window-inside-pixel-edges window))
|
||||||
|
;; Keep the grab point in the original text window.
|
||||||
|
(x (max (nth 0 edges) (min (1- (nth 2 edges)) (car point))))
|
||||||
|
(y (max (nth 1 edges) (min (1- (nth 3 edges)) (cdr point)))))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(when (desktop-demo-move
|
||||||
|
demo id (+ origin-x (round (- x (car start))))
|
||||||
|
(+ origin-y (* row-height
|
||||||
|
(round (/ (- y (cdr start))
|
||||||
|
(float row-height)))))
|
||||||
|
row-height)
|
||||||
|
(redisplay t)))))
|
||||||
|
(cond
|
||||||
|
(release (setq done t))
|
||||||
|
((mouse-movement-p next) nil)
|
||||||
|
(t (push next unread-command-events) (setq done t))))))))
|
||||||
|
(setf (desktop-demo-dragging demo) nil)))))
|
||||||
|
|
||||||
|
(defun desktop-demo--nudge (demo id dx dy)
|
||||||
|
"Activate DEMO's pane ID and move it DX columns and DY host rows."
|
||||||
|
(desktop-demo-activate demo id)
|
||||||
|
(let ((pane (desktop-demo--window demo id)))
|
||||||
|
(desktop-demo-move demo id
|
||||||
|
(+ (desktop-demo-window-x pane) (* dx (frame-char-width)))
|
||||||
|
(+ (desktop-demo-window-y pane) (* dy (frame-char-height))))))
|
||||||
|
|
||||||
|
(defun desktop-demo-keymap (demo id &optional title)
|
||||||
|
"Create DEMO's pane ID keymap; TITLE enables native live dragging."
|
||||||
|
(let ((press (if title (lambda () (desktop-demo-drag demo id last-input-event))
|
||||||
|
(lambda () (desktop-demo-activate demo id)))))
|
||||||
|
(ebox-keymap-create
|
||||||
|
:activate (lambda () (desktop-demo-activate demo id))
|
||||||
|
:bindings
|
||||||
|
(append
|
||||||
|
(mapcar (lambda (event) (cons event press))
|
||||||
|
'([down-mouse-1] [double-down-mouse-1] [triple-down-mouse-1]))
|
||||||
|
(list (cons "M-<left>" (lambda () (desktop-demo--nudge demo id -1 0)))
|
||||||
|
(cons "M-<right>" (lambda () (desktop-demo--nudge demo id 1 0)))
|
||||||
|
(cons "M-<up>" (lambda () (desktop-demo--nudge demo id 0 -1)))
|
||||||
|
(cons "M-<down>" (lambda () (desktop-demo--nudge demo id 0 1))))))))
|
||||||
|
|
||||||
|
(defun desktop-demo-reset (demo)
|
||||||
|
"Restore DEMO's original positions and document-order stack in one update."
|
||||||
|
(let* ((candidate (copy-desktop-demo demo))
|
||||||
|
(panes (mapcar #'copy-desktop-demo-window (desktop-demo-windows demo)))
|
||||||
|
(ids (mapcar #'desktop-demo-window-id panes))
|
||||||
|
(active (car (last ids))) updates)
|
||||||
|
(cl-loop for pane in panes for depth from 1 do
|
||||||
|
(setf (desktop-demo-window-x pane) 0 (desktop-demo-window-y pane) 0)
|
||||||
|
(push (list (desktop-demo-window-id pane)
|
||||||
|
:left (desktop-demo-window-left pane)
|
||||||
|
:top (desktop-demo-window-top pane) :z-index depth) updates)
|
||||||
|
(setq updates
|
||||||
|
(append updates (desktop-demo--chrome
|
||||||
|
demo pane (equal active (desktop-demo-window-id pane))))))
|
||||||
|
(setf (desktop-demo-windows candidate) panes (desktop-demo-order candidate) ids
|
||||||
|
(desktop-demo-active candidate) active (desktop-demo-serial candidate) (length ids))
|
||||||
|
(desktop-demo--publish demo candidate updates)))
|
||||||
|
|
||||||
|
(defun desktop-demo-toggle-theme (demo)
|
||||||
|
"Switch DEMO's complete palette without changing window geometry or order."
|
||||||
|
(let ((candidate (copy-desktop-demo demo)) updates)
|
||||||
|
(setf (desktop-demo-theme candidate)
|
||||||
|
(if (eq (desktop-demo-theme demo) 'dark) 'light 'dark))
|
||||||
|
(dolist (entry (desktop-demo-styles demo))
|
||||||
|
(dolist (match (ebox-selector-query-buffer
|
||||||
|
(current-buffer) (concat "." (symbol-name (car entry)))))
|
||||||
|
(when-let* ((handle (plist-get match :region-handle)))
|
||||||
|
(push (cons handle (desktop-demo-style candidate (car entry))) updates))))
|
||||||
|
(desktop-demo--publish
|
||||||
|
demo candidate
|
||||||
|
(append (nreverse updates)
|
||||||
|
(desktop-demo--chrome
|
||||||
|
candidate (desktop-demo--window demo (desktop-demo-active demo)) t)
|
||||||
|
(list (list "desktop-theme" :content
|
||||||
|
(if (eq (desktop-demo-theme candidate) 'light) "DARK" "LIGHT")))))))
|
||||||
|
|
||||||
|
(defun desktop-demo-control (demo action)
|
||||||
|
"Create a native toolbar command for DEMO's ACTION."
|
||||||
|
(ebox-keymap-create
|
||||||
|
:activate
|
||||||
|
(lambda ()
|
||||||
|
(pcase action
|
||||||
|
('reset (desktop-demo-reset demo))
|
||||||
|
('next (desktop-demo-activate demo (car (desktop-demo-order demo))))
|
||||||
|
('theme (desktop-demo-toggle-theme demo))
|
||||||
|
(_ (error "Unknown desktop action: %s" action))))))
|
||||||
|
|
||||||
|
(provide 'desktop-demo-reference)
|
||||||
|
;;; desktop-reference.el ends here
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
136
examples/interaction-reference.ebox
Normal file
136
examples/interaction-reference.ebox
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
;; Layout and property declarations. The matching .el file supplies state
|
||||||
|
;; and business actions; the runner loads it before evaluating this expression.
|
||||||
|
(let ((demo (interaction-demo-create)))
|
||||||
|
`(column :width (vw 100) :height (vh 95)
|
||||||
|
:padding ((lh 1) (ch 2))
|
||||||
|
:background-color "#F4F3ED" :color "#273B42"
|
||||||
|
(box :background-color "#153F4A" :color "#FFFFFF"
|
||||||
|
:padding ((lh 1) (ch 2))
|
||||||
|
(text :font-weight bold "EBOX / NATIVE INTERACTION LAB")
|
||||||
|
"\nFour node properties. Real commands. Incremental updates.")
|
||||||
|
(box :padding-block (lh 1)
|
||||||
|
"Wheel / C-v scroll. Hover to inspect. Click a control, or move point into it and press RET / SPC.\n"
|
||||||
|
"Solid borders mark Box surfaces, including padding. Gaps belong to no control.\n"
|
||||||
|
"Each section below isolates an effect; the live console combines all four.")
|
||||||
|
|
||||||
|
(box :font-weight bold "01 HELP + POINTER / hints without actions")
|
||||||
|
(flex :flex-wrap wrap :gap ((lh 1) (ch 2)) :padding-block (lh 1)
|
||||||
|
(box :width (ch 22) :padding ((lh 1) (ch 1))
|
||||||
|
:border "#90A9AB" :background-color "#FFFFFF"
|
||||||
|
:help-echo "Static help: this entire bordered Box supplies the same hint."
|
||||||
|
:pointer arrow
|
||||||
|
"ARROW\nStatic Box help")
|
||||||
|
(box :width (ch 22) :padding ((lh 1) (ch 1))
|
||||||
|
:border "#90A9AB" :background-color "#FFFFFF"
|
||||||
|
"TEXT POINTER\n"
|
||||||
|
(text :pointer text
|
||||||
|
:help-echo "This help belongs only to the underlined text, not its surrounding card."
|
||||||
|
:text-decoration-line underline
|
||||||
|
"Text-only hint"))
|
||||||
|
(box :width (ch 22) :padding ((lh 1) (ch 1))
|
||||||
|
:border "#90A9AB" :background-color "#FFFFFF"
|
||||||
|
:pointer hdrag
|
||||||
|
:help-echo "Pointer shape only: this is not a draggable splitter."
|
||||||
|
"HORIZONTAL DRAG\nShape demonstration"))
|
||||||
|
|
||||||
|
(box :font-weight bold "02 HOVER STYLE / appearance, no command")
|
||||||
|
(flex :flex-wrap wrap :gap ((lh 1) (ch 2)) :padding-block (lh 1) :pointer arrow
|
||||||
|
(box :id "hover-cool" :width (ch 22) :padding ((lh 1) (ch 1))
|
||||||
|
:border "#5893A3" :background-color "#E2EFF2"
|
||||||
|
:help-echo "Cool hover changes foreground/background. Font size and Box size stay fixed."
|
||||||
|
:hover-style (:color "#FFFFFF" :background-color "#286477")
|
||||||
|
"COOL FILL\nMove over the padding too")
|
||||||
|
(box :id "hover-warm" :width (ch 22) :padding ((lh 1) (ch 1))
|
||||||
|
:border "#B8874D" :background-color "#F8EDDC"
|
||||||
|
:help-echo "Warm hover adds an underline as well as color."
|
||||||
|
:hover-style (:color "#683D16" :background-color "#F6D6AB"
|
||||||
|
:text-decoration-line underline)
|
||||||
|
"WARM + UNDERLINE\nNo layout movement")
|
||||||
|
(box :width (ch 22) :padding ((lh 1) (ch 1))
|
||||||
|
:border "#A7ABA5" :background-color "#E9EAE5"
|
||||||
|
:help-echo "Control sample: no hover-style declaration."
|
||||||
|
"NO HOVER STYLE\nCompare the three cards"))
|
||||||
|
|
||||||
|
(box :font-weight bold "03 LIVE CONSOLE / native keymaps + region updates")
|
||||||
|
(box :padding ((lh 1) (ch 1)) :border "#5893A3"
|
||||||
|
:background-color "#FFFFFF"
|
||||||
|
(box :id "count" :color "#205A6C" :font-weight bold "COUNT 00")
|
||||||
|
(box :id "binding-state" "step 1 · four properties attached · cool hover")
|
||||||
|
(box :height (lh 1))
|
||||||
|
(flex :flex-wrap wrap :gap ((lh 1) (ch 2))
|
||||||
|
(box :id "live-action" :width (ch 23) :padding ((lh 1) (ch 1))
|
||||||
|
:border "#286477" :background-color "#E2EFF2"
|
||||||
|
:help-echo ,(ebox-help-create
|
||||||
|
(apply-partially #'interaction-demo-description demo))
|
||||||
|
:pointer hand
|
||||||
|
:hover-style ,(interaction-demo-hover demo) :keymap ,(interaction-demo-keymap demo)
|
||||||
|
(text :id "action-label" "ADD 1 [+] ")
|
||||||
|
(text :id "subtract-text"
|
||||||
|
:keymap ,(interaction-demo-subtract-keymap demo)
|
||||||
|
:help-echo ,(ebox-help-create
|
||||||
|
(apply-partially #'interaction-demo-description demo t))
|
||||||
|
"[-]"))
|
||||||
|
(box :id "step-control" :width (ch 23) :padding ((lh 1) (ch 1))
|
||||||
|
:border "#90A9AB" :background-color "#F0F4F2"
|
||||||
|
:pointer hand :help-echo "Replace the ADD control's keymap: +1 ↔ +5."
|
||||||
|
:hover-style (:background-color "#CCE0D6")
|
||||||
|
:keymap ,(interaction-demo-action demo 'step)
|
||||||
|
"SWITCH STEP 1 / 5")
|
||||||
|
(box :id "binding-control" :width (ch 23) :padding ((lh 1) (ch 1))
|
||||||
|
:border "#90A9AB" :background-color "#F0F4F2"
|
||||||
|
:pointer hand :help-echo "Remove or restore all four properties on the ADD control."
|
||||||
|
:hover-style (:background-color "#CCE0D6")
|
||||||
|
:keymap ,(interaction-demo-action demo 'binding)
|
||||||
|
"REMOVE / RESTORE")
|
||||||
|
(box :id "palette-control" :width (ch 23) :padding ((lh 1) (ch 1))
|
||||||
|
:border "#B8874D" :background-color "#F8EDDC"
|
||||||
|
:pointer hand :help-echo "Replace the ADD control's hover-style while preserving its geometry."
|
||||||
|
:hover-style (:background-color "#F6D6AB")
|
||||||
|
:keymap ,(interaction-demo-action demo 'palette)
|
||||||
|
"SWITCH HOVER PALETTE")
|
||||||
|
(box :id "group-control" :width (ch 23) :padding ((lh 1) (ch 1))
|
||||||
|
:border "#789783" :background-color "#E9F0E9"
|
||||||
|
:pointer hand :help-echo "Update all three .demo-status regions with one selector call."
|
||||||
|
:hover-style (:background-color "#C8DDC9")
|
||||||
|
:keymap ,(interaction-demo-action demo 'group)
|
||||||
|
"UPDATE STATUS GROUP"))
|
||||||
|
(row :item-gap (ch 2) :padding-block (lh 1)
|
||||||
|
"Class targets:"
|
||||||
|
(box :id "status-a" :class demo-status :color "#85652F" "WAIT A")
|
||||||
|
(box :id "status-b" :class demo-status :color "#85652F" "WAIT B")
|
||||||
|
(box :id "status-c" :class demo-status :color "#85652F" "WAIT C"))
|
||||||
|
(box :padding-block (lh 1)
|
||||||
|
"Text can be interactive too: "
|
||||||
|
(text :id "reset-text" :pointer hand :keymap ,(interaction-demo-action demo 'reset)
|
||||||
|
:help-echo "Reset count through a Text node's keymap."
|
||||||
|
:color "#286477" :text-decoration-line underline
|
||||||
|
:hover-style (:color "#9A531C")
|
||||||
|
"[ RESET COUNT ]")))
|
||||||
|
|
||||||
|
(box :height (lh 1))
|
||||||
|
(box :font-weight bold "04 NESTED OWNERSHIP / parent, child, explicit nil")
|
||||||
|
(box :id "parent-surface" :padding ((lh 1) (ch 2))
|
||||||
|
:border "#789783" :background-color "#E9F0E9"
|
||||||
|
:pointer hand :help-echo "Parent surface: clicking its padding records PARENT."
|
||||||
|
:hover-style (:background-color "#C8DDC9")
|
||||||
|
:keymap ,(interaction-demo-action demo 'parent)
|
||||||
|
"PARENT SURFACE — click here or in its padding\n"
|
||||||
|
(text :id "child-text" :pointer hand
|
||||||
|
:help-echo "Child owns its binding: this records CHILD, not PARENT."
|
||||||
|
:hover-style (:color "#FFFFFF" :background-color "#466B55")
|
||||||
|
:keymap ,(interaction-demo-action demo 'child)
|
||||||
|
"[ CHILD ACTION ]")
|
||||||
|
" "
|
||||||
|
(text :id "quiet-text" :help-echo nil :pointer nil
|
||||||
|
:hover-style nil :keymap nil
|
||||||
|
"[ EXPLICIT NIL: no inherited interaction ]"))
|
||||||
|
|
||||||
|
(box :height (lh 1))
|
||||||
|
(box :font-weight bold "EVENT LOG / newest first")
|
||||||
|
(box :id "event-log" :height (lh 4) :padding-inline (ch 1)
|
||||||
|
:background-color "#153F4A" :color "#E2EFF2"
|
||||||
|
"No actions yet.\n—\n—\n—")
|
||||||
|
(box :padding-block (lh 1) :color "#586B6A"
|
||||||
|
"Commands update named regions; they do not rewrite buffer text.\n"
|
||||||
|
"Keymaps do not create a focus system: move point normally to use local keys.\n"
|
||||||
|
"Hint display and exact pointer shapes follow your Emacs settings and window system.")))
|
||||||
151
examples/interaction-reference.el
Normal file
151
examples/interaction-reference.el
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
;;; interaction-reference.el --- State and actions for the interaction lab -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Loaded by the generic playground runner before interaction-reference.ebox.
|
||||||
|
;; Each DSL evaluation creates its own state. Ebox supplies command/keymap
|
||||||
|
;; construction, mouse-buffer targeting, and named-region updates.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ebox)
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'seq)
|
||||||
|
(require 'subr-x)
|
||||||
|
|
||||||
|
(cl-defstruct (interaction-demo
|
||||||
|
(:constructor interaction-demo--make))
|
||||||
|
"Application state belonging to one interaction-lab preview."
|
||||||
|
(count 0) (step 1) (enabled t) warm events (serial 0) keymap subtract-keymap)
|
||||||
|
|
||||||
|
(defun interaction-demo-create ()
|
||||||
|
"Create independent state and bindings for a new preview."
|
||||||
|
(let ((demo (interaction-demo--make)))
|
||||||
|
(setf (interaction-demo-keymap demo)
|
||||||
|
(interaction-demo--step-map demo)
|
||||||
|
(interaction-demo-subtract-keymap demo)
|
||||||
|
(interaction-demo--step-map demo t))
|
||||||
|
demo))
|
||||||
|
|
||||||
|
(defun interaction-demo-description (demo &optional subtract)
|
||||||
|
"Return DEMO's current action hint, describing subtraction when SUBTRACT."
|
||||||
|
(format "Live help: count = %d; this action %s %d. RET / SPC / click."
|
||||||
|
(interaction-demo-count demo) (if subtract "subtracts" "adds")
|
||||||
|
(interaction-demo-step demo)))
|
||||||
|
|
||||||
|
(defun interaction-demo--record (demo description)
|
||||||
|
"Record DEMO's action DESCRIPTION and refresh its four-line event log."
|
||||||
|
(push (format "%02d %s" (cl-incf (interaction-demo-serial demo))
|
||||||
|
description)
|
||||||
|
(interaction-demo-events demo))
|
||||||
|
(setf (interaction-demo-events demo)
|
||||||
|
(seq-take (interaction-demo-events demo) 4))
|
||||||
|
(let ((events (interaction-demo-events demo)))
|
||||||
|
(ebox-region-update
|
||||||
|
"event-log" :content
|
||||||
|
(string-join (append events (make-list (- 4 (length events)) "—")) "\n"))))
|
||||||
|
|
||||||
|
(defun interaction-demo--show-count (demo)
|
||||||
|
"Refresh the displayed count for DEMO."
|
||||||
|
(ebox-region-update "count" :content
|
||||||
|
(format "COUNT %02d" (interaction-demo-count demo))))
|
||||||
|
|
||||||
|
(defun interaction-demo--add (demo amount)
|
||||||
|
"Add AMOUNT to DEMO's count and record the action."
|
||||||
|
(cl-incf (interaction-demo-count demo) amount)
|
||||||
|
(interaction-demo--show-count demo)
|
||||||
|
(interaction-demo--record
|
||||||
|
demo (format "%+d → count %d" amount (interaction-demo-count demo))))
|
||||||
|
|
||||||
|
(defun interaction-demo-hover (demo)
|
||||||
|
"Return DEMO's chosen hover palette using ordinary Ebox declarations."
|
||||||
|
(if (interaction-demo-warm demo)
|
||||||
|
'(:background-color "#F6D6AB" :color "#683D16"
|
||||||
|
:text-decoration-line underline)
|
||||||
|
'(:background-color "#BCDDE5" :color "#123E4B"
|
||||||
|
:text-decoration-line underline)))
|
||||||
|
|
||||||
|
(defun interaction-demo--step-map (demo &optional subtract)
|
||||||
|
"Create DEMO's captured step map, with SUBTRACT choosing negative activation."
|
||||||
|
(let ((amount (interaction-demo-step demo)))
|
||||||
|
(ebox-keymap-create
|
||||||
|
:activate (lambda ()
|
||||||
|
(interaction-demo--add demo (if subtract (- amount) amount)))
|
||||||
|
:bindings
|
||||||
|
(list (cons "+" (lambda () (interaction-demo--add demo amount)))
|
||||||
|
(cons "-" (lambda () (interaction-demo--add demo (- amount))))))))
|
||||||
|
|
||||||
|
(defun interaction-demo--refresh (demo)
|
||||||
|
"Publish DEMO's current action properties and binding summary."
|
||||||
|
(let ((enabled (interaction-demo-enabled demo))
|
||||||
|
(step (interaction-demo-step demo)))
|
||||||
|
(ebox-region-update
|
||||||
|
"live-action"
|
||||||
|
:help-echo (and enabled
|
||||||
|
(ebox-help-create
|
||||||
|
(apply-partially #'interaction-demo-description demo)))
|
||||||
|
:pointer (and enabled 'hand)
|
||||||
|
:hover-style (and enabled (interaction-demo-hover demo))
|
||||||
|
:keymap (and enabled (interaction-demo-keymap demo)))
|
||||||
|
(ebox-region-update
|
||||||
|
"action-label" :content
|
||||||
|
(if enabled (format "ADD %d [+] " step) "BINDING REMOVED "))
|
||||||
|
(ebox-region-update
|
||||||
|
"subtract-text"
|
||||||
|
:help-echo (and enabled
|
||||||
|
(ebox-help-create
|
||||||
|
(apply-partially #'interaction-demo-description demo t)))
|
||||||
|
:keymap (and enabled (interaction-demo-subtract-keymap demo))))
|
||||||
|
(ebox-region-update
|
||||||
|
"binding-state" :content
|
||||||
|
(format "step %d · %s · %s hover"
|
||||||
|
(interaction-demo-step demo)
|
||||||
|
(if (interaction-demo-enabled demo)
|
||||||
|
"four properties attached" "four properties nil")
|
||||||
|
(if (interaction-demo-warm demo) "warm" "cool"))))
|
||||||
|
|
||||||
|
(defun interaction-demo-action (demo action)
|
||||||
|
"Return a native activation keymap for DEMO's named ACTION."
|
||||||
|
(ebox-keymap-create
|
||||||
|
:activate
|
||||||
|
(lambda ()
|
||||||
|
(pcase action
|
||||||
|
('step
|
||||||
|
(setf (interaction-demo-step demo)
|
||||||
|
(if (= (interaction-demo-step demo) 1) 5 1)
|
||||||
|
(interaction-demo-keymap demo)
|
||||||
|
(interaction-demo--step-map demo)
|
||||||
|
(interaction-demo-subtract-keymap demo)
|
||||||
|
(interaction-demo--step-map demo t))
|
||||||
|
(interaction-demo--refresh demo)
|
||||||
|
(interaction-demo--record
|
||||||
|
demo (format "Replaced keymap: step %d" (interaction-demo-step demo))))
|
||||||
|
('binding
|
||||||
|
(setf (interaction-demo-enabled demo)
|
||||||
|
(not (interaction-demo-enabled demo)))
|
||||||
|
(interaction-demo--refresh demo)
|
||||||
|
(interaction-demo--record
|
||||||
|
demo (if (interaction-demo-enabled demo)
|
||||||
|
"Restored all four properties" "Removed all four properties")))
|
||||||
|
('palette
|
||||||
|
(setf (interaction-demo-warm demo)
|
||||||
|
(not (interaction-demo-warm demo)))
|
||||||
|
(interaction-demo--refresh demo)
|
||||||
|
(interaction-demo--record
|
||||||
|
demo (if (interaction-demo-warm demo)
|
||||||
|
"Hover → warm palette" "Hover → cool palette")))
|
||||||
|
('reset
|
||||||
|
(setf (interaction-demo-count demo) 0)
|
||||||
|
(interaction-demo--show-count demo)
|
||||||
|
(interaction-demo--record demo "Reset count"))
|
||||||
|
('group
|
||||||
|
(ebox-update-selector (current-buffer) ".demo-status"
|
||||||
|
:content "READY" :color "#287A52"
|
||||||
|
:help-echo "This status was updated with its class group.")
|
||||||
|
(interaction-demo--record demo "Updated three .demo-status regions in one batch"))
|
||||||
|
('parent (interaction-demo--record demo "PARENT surface activated"))
|
||||||
|
('child (interaction-demo--record demo "CHILD text activated"))
|
||||||
|
(_ (error "Unknown interaction-lab action: %S" action))))))
|
||||||
|
|
||||||
|
(provide 'interaction-demo-reference)
|
||||||
|
|
||||||
|
;;; interaction-reference.el ends here
|
||||||
9
examples/layer-minimal.ebox
Normal file
9
examples/layer-minimal.ebox
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
'(box :width (ch 32) :height (lh 5)
|
||||||
|
:background-color "#233044"
|
||||||
|
(box :color "#cbd5e1"
|
||||||
|
"01234567890123456789012345678901\nabcdefghijklmnopqrstuvwxyzABCDEF\n01234567890123456789012345678901\nabcdefghijklmnopqrstuvwxyzABCDEF\n01234567890123456789012345678901")
|
||||||
|
(box :position absolute :left (ch 6) :top (lh 1)
|
||||||
|
:width (ch 18) :height (lh 3) :z-index 1
|
||||||
|
:background-color "#2563eb" :color "white"
|
||||||
|
:padding-inline (ch 1)
|
||||||
|
"Upper layer\nCovers the base\nBase stays alive"))
|
||||||
252
examples/layer-reference.ebox
Normal file
252
examples/layer-reference.ebox
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
;; Layout data only. The matching companion owns state and business actions.
|
||||||
|
(let ((demo (layer-demo-create)))
|
||||||
|
`(column
|
||||||
|
:width (vw 100) :height (vh 100)
|
||||||
|
:padding ((lh 1) (ch 2))
|
||||||
|
:background-color "#101117" :color "#DCE0EC"
|
||||||
|
(flex :flex-wrap wrap :gap ((lh 0) (ch 2)) :padding-bottom (lh 1)
|
||||||
|
(box :padding-inline (ch 1) :background-color "#D7FA73"
|
||||||
|
:color "#151A0C" :font-weight bold "ORBIT / CREATIVE OS")
|
||||||
|
(box :color "#A99BFF" "VOL. 007 / SPATIAL EDITION"))
|
||||||
|
(box :padding ((lh 1) (ch 2)) :background-color "#1A1D28"
|
||||||
|
(box :font-weight bold :color "#F7F8FF"
|
||||||
|
"MAKE ROOM\nFOR THE UNEXPECTED.")
|
||||||
|
(box :padding-top (lh 1) :color "#AEB6CE"
|
||||||
|
"A desktop of intersecting ideas.\nPull it apart. Shift the focus. Change the atmosphere."))
|
||||||
|
(flex :flex-wrap wrap :gap ((lh 1) (ch 1)) :padding-block (lh 1)
|
||||||
|
:font-weight bold :color "#101117"
|
||||||
|
(box :padding-inline (ch 1) :background-color "#D7FA73"
|
||||||
|
:pointer hand :hover-style (:background-color "#FFFFFF")
|
||||||
|
:help-echo "Separate the three cards, then bring them back into their original stack."
|
||||||
|
:keymap ,(layer-demo-action demo 'explode) "EXPLODE STACK")
|
||||||
|
(box :padding-inline (ch 1) :background-color "#A99BFF"
|
||||||
|
:pointer hand :hover-style (:background-color "#FFFFFF")
|
||||||
|
:help-echo "Bring Studio, Next, and Signal to the front in turn."
|
||||||
|
:keymap ,(layer-demo-action demo 'focus) "CYCLE FOCUS")
|
||||||
|
(box :padding-inline (ch 1) :background-color "#B9E9F2"
|
||||||
|
:pointer hand :hover-style (:background-color "#FFFFFF")
|
||||||
|
:help-echo "Switch between electric violet / ice / lime and a warmer studio palette."
|
||||||
|
:keymap ,(layer-demo-action demo 'mood) "SWITCH MOOD"))
|
||||||
|
|
||||||
|
;; Each card is a local stacking group. The dark slabs are separate
|
||||||
|
;; backplates; moving the cards deliberately reveals the composition.
|
||||||
|
(box :id "orbit-canvas" :height (lh 25) :background-color "#101117"
|
||||||
|
(box :position absolute :left (ch 1) :top (lh 0)
|
||||||
|
:width (ch 24) :height (lh 1) :color "#9099B4"
|
||||||
|
"FIELD / 007 + +")
|
||||||
|
(box :position absolute :left (% 75) :top (lh 0)
|
||||||
|
:width (ch 18) :height (lh 1) :color "#D7FA73"
|
||||||
|
"COMPOSITION / LIVE")
|
||||||
|
(box :position absolute :left (ch 0) :top (lh 2)
|
||||||
|
:width (ch 2) :height (lh 21) :color "#4E566E"
|
||||||
|
"00\n |\n |\n |\n04\n |\n |\n |\n08\n |\n |\n |\n12\n |\n |\n |\n16\n |\n |\n |\n20")
|
||||||
|
(box :position absolute :left (ch 8) :top (lh 5)
|
||||||
|
:width (ch 68) :height (lh 16) :background-color "#32354B"
|
||||||
|
(box :position absolute :left (ch 2) :top (lh 1)
|
||||||
|
:width (ch 64) :height (lh 14) :background-color "#202333"
|
||||||
|
(box :position absolute :left (ch 1) :top (lh 12)
|
||||||
|
:width (ch 32) :height (lh 1) :color "#8B92AE"
|
||||||
|
"+ LOCAL SPACE / INFINITE IDEAS")))
|
||||||
|
(box :position absolute :left (ch 6) :top (lh 4)
|
||||||
|
:width (ch 40) :height (lh 12) :z-index 1
|
||||||
|
:background-color "#06070B")
|
||||||
|
(box :position absolute :left (ch 48) :top (lh 3)
|
||||||
|
:width (ch 28) :height (lh 9) :z-index 1
|
||||||
|
:background-color "#06070B")
|
||||||
|
(box :position absolute :left (ch 22) :top (lh 8)
|
||||||
|
:width (ch 40) :height (lh 13) :z-index 1
|
||||||
|
:background-color "#06070B")
|
||||||
|
|
||||||
|
(box :id "orbit-violet" :position absolute :left (ch 4) :top (lh 3)
|
||||||
|
:width (ch 40) :height (lh 12) :z-index 2
|
||||||
|
:padding-inline (ch 2) :background-color "#A99BFF" :color "#201A38"
|
||||||
|
:help-echo "Studio is a local group: its small lime badge cannot escape above a higher sibling card."
|
||||||
|
(box :padding-top (lh 1) :font-weight bold "STUDIO / 01")
|
||||||
|
(box :padding-top (lh 1) "FORM FOLLOWS\nIMAGINATION.")
|
||||||
|
(box :position absolute :left (ch 2) :top (lh 5)
|
||||||
|
:width (ch 32) :height (lh 5) :background-color "#30284E"
|
||||||
|
:color "#D4C9FF"
|
||||||
|
(box :position absolute :left (ch 2) :top (lh 1)
|
||||||
|
:width (ch 14) :height (lh 3) :background-color "#645293"
|
||||||
|
" [ [ + ] ]\n\n SHAPE / PLAY")
|
||||||
|
(box :position absolute :left (ch 12) :top (lh 0)
|
||||||
|
:width (ch 12) :height (lh 3) :z-index 1
|
||||||
|
:background-color "#D7CFFF" :color "#30284E"
|
||||||
|
" + +\n / /\n + +")
|
||||||
|
(box :position absolute :left (ch 20) :top (lh 2)
|
||||||
|
:width (ch 10) :height (lh 2) :z-index 60
|
||||||
|
:background-color "#D7FA73" :color "#202816"
|
||||||
|
:text-align center "DEPTH\n03"))
|
||||||
|
(box :position absolute :left (ch 2) :top (lh 10)
|
||||||
|
:width (ch 30) :height (lh 1) "OBJECT 01 / EDITION 007"))
|
||||||
|
|
||||||
|
(box :id "orbit-lime" :position absolute :left (ch 46) :top (lh 2)
|
||||||
|
:width (ch 28) :height (lh 9) :z-index 3
|
||||||
|
:padding-inline (ch 2) :background-color "#D7FA73" :color "#202816"
|
||||||
|
:help-echo "Next is a separate sibling card. Cycle Focus to move its whole local composition forward."
|
||||||
|
(box :padding-top (lh 1) :font-weight bold "NEXT / 02")
|
||||||
|
(box :padding-top (lh 1) "LESS NOISE.\nMORE POSSIBILITY.")
|
||||||
|
(box :position absolute :left (ch 15) :top (lh 5)
|
||||||
|
:width (ch 9) :height (lh 3) :background-color "#24301A"
|
||||||
|
:color "#D7FA73" :text-align center " /\n /___\n / ")
|
||||||
|
(box :position absolute :left (ch 2) :top (lh 6)
|
||||||
|
:width (ch 12) :height (lh 2) "OPEN SPACE\n24 / 7"))
|
||||||
|
|
||||||
|
(box :id "orbit-signal" :position absolute :left (ch 20) :top (lh 7)
|
||||||
|
:width (ch 40) :height (lh 13) :z-index 4
|
||||||
|
:padding-inline (ch 2) :background-color "#B9E9F2" :color "#173139"
|
||||||
|
:help-echo "Signal starts in front. Its bars, labels, and floating badge move together as one local stack."
|
||||||
|
(box :padding-top (lh 1) :font-weight bold "SIGNAL / NOW PLAYING")
|
||||||
|
(box :padding-top (lh 1) "AFTER HOURS\nA soundtrack for what comes next.")
|
||||||
|
(box :position absolute :left (ch 2) :top (lh 5)
|
||||||
|
:width (ch 32) :height (lh 5) :background-color "#173139"
|
||||||
|
:color "#B9E9F2"
|
||||||
|
(box :position absolute :left (ch 2) :top (lh 3)
|
||||||
|
:width (ch 3) :height (lh 1) :background-color "#B9E9F2")
|
||||||
|
(box :position absolute :left (ch 6) :top (lh 2)
|
||||||
|
:width (ch 3) :height (lh 2) :background-color "#A99BFF")
|
||||||
|
(box :position absolute :left (ch 10) :top (lh 1)
|
||||||
|
:width (ch 3) :height (lh 3) :background-color "#D7FA73")
|
||||||
|
(box :position absolute :left (ch 14) :top (lh 0)
|
||||||
|
:width (ch 3) :height (lh 4) :background-color "#B9E9F2")
|
||||||
|
(box :position absolute :left (ch 18) :top (lh 2)
|
||||||
|
:width (ch 3) :height (lh 2) :background-color "#A99BFF")
|
||||||
|
(box :position absolute :left (ch 22) :top (lh 1)
|
||||||
|
:width (ch 3) :height (lh 3) :background-color "#D7FA73")
|
||||||
|
(box :position absolute :left (ch 26) :top (lh 3)
|
||||||
|
:width (ch 3) :height (lh 1) :background-color "#B9E9F2")
|
||||||
|
(box :position absolute :left (ch 20) :top (lh 0)
|
||||||
|
:width (ch 10) :height (lh 1) :z-index 8
|
||||||
|
:background-color "#F9FCF3" :color "#173139"
|
||||||
|
:text-align center "ON AIR +"))
|
||||||
|
(box :position absolute :left (ch 2) :top (lh 11)
|
||||||
|
:width (ch 32) :height (lh 1) "03:42 ========---- 05:18"))
|
||||||
|
|
||||||
|
(box :position absolute :left (ch 61) :top (lh 15)
|
||||||
|
:width (ch 15) :height (lh 3) :z-index 5
|
||||||
|
:padding-inline (ch 1) :background-color "#F6F5EF" :color "#282B35"
|
||||||
|
"+ IN SYNC\n3 OPEN IDEAS\nSHARED SPACE")
|
||||||
|
(box :position absolute :left (ch 3) :top (lh 22)
|
||||||
|
:width (ch 72) :height (lh 1) :color "#A99BFF"
|
||||||
|
"+ . + . + . + . + . + . + . + . + . + . + .")
|
||||||
|
(box :position absolute :left (ch 3) :top (lh 24)
|
||||||
|
:width (ch 70) :height (lh 1) :color "#9099B4"
|
||||||
|
"ORBIT / 007 COMPOSE IN LAYERS. THINK IN POSSIBILITIES."))
|
||||||
|
(box :id "orbit-state" :padding-inline (ch 1)
|
||||||
|
:background-color "#222637" :color "#D7FA73"
|
||||||
|
,(layer-demo-orbit-status demo))
|
||||||
|
(box :padding-top (lh 2) :font-weight bold :color "#F7F8FF"
|
||||||
|
"EBOX / LAYER LAB")
|
||||||
|
(box :color "#AEB6CE" "Retained content, opaque overlap, native interaction.")
|
||||||
|
(box :padding-block (lh 1)
|
||||||
|
"Click a control, or move point into it and press RET / SPC.\n"
|
||||||
|
"All text shares one font size. Scroll with the wheel or C-v when needed.")
|
||||||
|
|
||||||
|
(box "01 / LAYERED DESK absolute position + local stacking")
|
||||||
|
(flex :flex-wrap wrap :gap ((lh 1) (ch 1)) :padding-block (lh 1)
|
||||||
|
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||||
|
:pointer hand :hover-style (:background-color "#505B7A")
|
||||||
|
:help-echo "Hide or show Panel A. Revealing a lower layer shows its latest content."
|
||||||
|
:keymap ,(layer-demo-action demo 'toggle) "TOGGLE PANEL A")
|
||||||
|
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||||
|
:pointer hand :hover-style (:background-color "#505B7A")
|
||||||
|
:help-echo "Move Panel A between left 4ch / top 2lh and left 10ch / top 0lh."
|
||||||
|
:keymap ,(layer-demo-action demo 'move) "MOVE PANEL A")
|
||||||
|
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||||
|
:pointer hand :hover-style (:background-color "#505B7A")
|
||||||
|
:help-echo "Switch Panel A between z=2 and z=4; Panel B stays at z=3."
|
||||||
|
:keymap ,(layer-demo-action demo 'raise) "RAISE / LOWER A")
|
||||||
|
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||||
|
:pointer hand :hover-style (:background-color "#505B7A")
|
||||||
|
:help-echo "Update the lower note even when every part of it is covered."
|
||||||
|
:keymap ,(layer-demo-action demo 'update-lower) "UPDATE COVERED NOTE"))
|
||||||
|
(box :id "layer-desk" :height (lh 10) :background-color "#222637"
|
||||||
|
(box :height (lh 10)
|
||||||
|
"DESK / ordinary content stays in its flow slot\n"
|
||||||
|
"........................................................................\n"
|
||||||
|
"........................................................................\n"
|
||||||
|
"........................................................................\n"
|
||||||
|
"........................................................................\n"
|
||||||
|
"........................................................................\n"
|
||||||
|
"........................................................................\n"
|
||||||
|
"........................................................................\n"
|
||||||
|
"........................................................................\n"
|
||||||
|
"DESK END / panels do not add flow rows")
|
||||||
|
(box :id "lower-note" :position absolute :left (ch 4) :top (lh 2)
|
||||||
|
:width (ch 38) :height (lh 5) :z-index 0 :padding-inline (ch 1)
|
||||||
|
:background-color "#C9DED0" :color "#244F36" :pointer hand
|
||||||
|
:hover-style (:background-color "#ADCAB7")
|
||||||
|
:help-echo ,(ebox-help-create
|
||||||
|
(apply-partially #'layer-demo-help demo 'lower))
|
||||||
|
:keymap ,(layer-demo-action demo 'lower)
|
||||||
|
,(layer-demo-lower-content demo))
|
||||||
|
(box :id "panel-a" :position absolute :left (ch 4) :top (lh 2)
|
||||||
|
:width (ch 38) :height (lh 5) :z-index 2 :padding-inline (ch 1)
|
||||||
|
:background-color "#3A687E" :color "#FFFFFF"
|
||||||
|
:help-echo "Panel A is opaque, including its blank padding. Use the toolbar to move or raise it."
|
||||||
|
"PANEL A / blue\nMove and raise from the toolbar.\nA survives under the amber panel.\nOpaque padding covers too.\nThe covered note remains alive.")
|
||||||
|
(box :id "panel-b" :position absolute :left (ch 22) :top (lh 4)
|
||||||
|
:width (ch 28) :height (lh 4) :z-index 3 :padding-inline (ch 1)
|
||||||
|
:background-color "#D7AF6D" :color "#503B21"
|
||||||
|
:help-echo "Panel B stays at z=3. Raising A changes which panel owns the overlap."
|
||||||
|
"PANEL B / amber\nFixed at z=3.\nLater layers cover earlier\npaint, not logical data."))
|
||||||
|
(box :height (lh 1))
|
||||||
|
|
||||||
|
(box "02 / RELATIVE POSITION reserve the slot, shift the paint")
|
||||||
|
(column :height (lh 4) :background-color "#292437"
|
||||||
|
(box :id "relative-marker" :position relative :left (ch 4) :top (lh 1)
|
||||||
|
:width (ch 24) :height (lh 1) :z-index 1
|
||||||
|
:background-color "#765D87" :color "#FFFFFF" "RELATIVE / +4ch, +1lh")
|
||||||
|
(box "flow row 2: the shifted marker covers part of this row")
|
||||||
|
(box :id "relative-following" "FLOW ROW 3 / the original slot was kept")
|
||||||
|
(box "Each parent keeps a local stacking group automatically."))
|
||||||
|
(box :height (lh 1))
|
||||||
|
|
||||||
|
(box "03 / ANCHORED MENU nested owner + root projection")
|
||||||
|
(box :padding-block (lh 1)
|
||||||
|
"The owner below clips to one row. Its menu stays a logical child\n"
|
||||||
|
"but paints in the root layer, outside that local clip.")
|
||||||
|
(flex :gap ((lh 0) (ch 1)) :padding-bottom (lh 1)
|
||||||
|
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||||
|
:pointer hand :hover-style (:background-color "#505B7A")
|
||||||
|
:help-echo "Cycle bottom-start, bottom-end, top-start, and top-end. Open the menu to compare."
|
||||||
|
:keymap ,(layer-demo-action demo 'placement) "CYCLE PLACEMENT")
|
||||||
|
(box :padding-inline (ch 1) :background-color "#343B52"
|
||||||
|
:pointer hand :hover-style (:background-color "#505B7A")
|
||||||
|
:help-echo "Move the relative trigger from left 4ch to 12ch. The open menu follows its anchor."
|
||||||
|
:keymap ,(layer-demo-action demo 'anchor) "MOVE ANCHOR"))
|
||||||
|
(box :height (lh 3) :background-color "#1C2430"
|
||||||
|
"SPACE ABOVE / top placements can cover this area\n"
|
||||||
|
"........................................................................\n"
|
||||||
|
"........................................................................")
|
||||||
|
(box :id "menu-owner" :height (lh 1) :overflow hidden :background-color "#303D43"
|
||||||
|
(box :id "menu-trigger" :position relative :left (ch 4) :top (lh 0)
|
||||||
|
:width (ch 32) :height (lh 1) :padding-inline (ch 1)
|
||||||
|
:background-color "#286347" :color "#FFFFFF" :pointer hand
|
||||||
|
:hover-style (:background-color "#407E5C")
|
||||||
|
:help-echo ,(ebox-help-create
|
||||||
|
(apply-partially #'layer-demo-help demo 'menu))
|
||||||
|
:keymap ,(layer-demo-action demo 'menu) "OPEN / CLOSE ROOT MENU")
|
||||||
|
(box :id "root-menu" :position absolute :layer root :anchor "menu-trigger"
|
||||||
|
:placement bottom-start :z-index 50 :visibility hidden
|
||||||
|
:width (ch 26) :height (lh 3) :background-color "#153F4A" :color "#FFFFFF"
|
||||||
|
(box :padding-inline (ch 1) "ROOT MENU / choose")
|
||||||
|
(box :id "menu-sage" :padding-inline (ch 1) :pointer hand
|
||||||
|
:hover-style (:background-color "#407E5C")
|
||||||
|
:help-echo "Choose sage, update the result, and close the menu."
|
||||||
|
:keymap ,(layer-demo-action demo 'sage) "CHOOSE SAGE")
|
||||||
|
(box :id "menu-amber" :padding-inline (ch 1) :pointer hand
|
||||||
|
:hover-style (:background-color "#895314")
|
||||||
|
:help-echo "Choose amber, update the result, and close the menu."
|
||||||
|
:keymap ,(layer-demo-action demo 'amber) "CHOOSE AMBER")))
|
||||||
|
(box :height (lh 3) :background-color "#1C2430"
|
||||||
|
"SPACE BELOW / bottom placements can cover this area\n"
|
||||||
|
"........................................................................\n"
|
||||||
|
"........................................................................")
|
||||||
|
(box :id "menu-result" :padding-block (lh 1) "No menu selection yet.")
|
||||||
|
(box :id "layer-status" :background-color "#153F4A" :color "#FFFFFF"
|
||||||
|
:padding-inline (ch 1) ,(layer-demo-status demo))
|
||||||
|
(box :padding-top (lh 1)
|
||||||
|
"Try: update the covered note twice, hide A, then activate the revealed note.\n"
|
||||||
|
"Open the menu, cycle all four placements, move its anchor, and select a color.\n"
|
||||||
|
"Commands publish named-region updates. Layers do not create a focus system.")))
|
||||||
145
examples/layer-reference.el
Normal file
145
examples/layer-reference.el
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
;;; layer-reference.el --- State and actions for the layer lab -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; The matching .ebox file owns every layout declaration. Each evaluation
|
||||||
|
;; creates independent application state; public Ebox adapters dispatch native
|
||||||
|
;; commands and help in the preview buffer and update semantic region IDs.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ebox)
|
||||||
|
|
||||||
|
(cl-defstruct (layer-demo (:constructor layer-demo-create))
|
||||||
|
"Application state belonging to one layer-lab preview."
|
||||||
|
(visible t) moved raised (revision 0) (lower-clicks 0)
|
||||||
|
menu-visible (placement 0) anchor-moved (selection "none")
|
||||||
|
exploded (focus 2) warm)
|
||||||
|
|
||||||
|
(defun layer-demo-orbit-status (demo)
|
||||||
|
"Describe the spatial desktop controls for DEMO."
|
||||||
|
(format "%s / %s IN FRONT / %s PALETTE"
|
||||||
|
(if (layer-demo-exploded demo) "EXPANDED" "STACKED")
|
||||||
|
(aref ["STUDIO" "NEXT" "SIGNAL"] (layer-demo-focus demo))
|
||||||
|
(if (layer-demo-warm demo) "SOLAR" "LUNAR")))
|
||||||
|
|
||||||
|
(defconst layer-demo-placements
|
||||||
|
[bottom-start bottom-end top-start top-end]
|
||||||
|
"Anchor placements visited by the lab's placement control.")
|
||||||
|
|
||||||
|
(defun layer-demo-lower-content (demo)
|
||||||
|
"Return the covered note's current content for DEMO."
|
||||||
|
(format "LOWER REV %02d\nUpdated even while covered.\nReveal me, then click this surface.\nLower activations: %02d\nThe logical note never left the tree."
|
||||||
|
(layer-demo-revision demo) (layer-demo-lower-clicks demo)))
|
||||||
|
|
||||||
|
(defun layer-demo-status (demo)
|
||||||
|
"Describe DEMO's currently published panel and menu state."
|
||||||
|
(format "A: %s / %s / z=%d | lower rev=%02d | menu: %s / %s | choice: %s"
|
||||||
|
(if (layer-demo-visible demo) "shown" "hidden")
|
||||||
|
(if (layer-demo-moved demo) "moved" "home")
|
||||||
|
(if (layer-demo-raised demo) 4 2)
|
||||||
|
(layer-demo-revision demo)
|
||||||
|
(if (layer-demo-menu-visible demo) "open" "closed")
|
||||||
|
(aref layer-demo-placements (layer-demo-placement demo))
|
||||||
|
(layer-demo-selection demo)))
|
||||||
|
|
||||||
|
(defun layer-demo-help (demo subject)
|
||||||
|
"Return live native help for DEMO's SUBJECT."
|
||||||
|
(pcase subject
|
||||||
|
('lower (format "Lower note revision %d; activated %d times. RET / SPC / click."
|
||||||
|
(layer-demo-revision demo) (layer-demo-lower-clicks demo)))
|
||||||
|
('menu (format "Menu is %s; placement %s; selected %s. RET / SPC / click."
|
||||||
|
(if (layer-demo-menu-visible demo) "open" "closed")
|
||||||
|
(aref layer-demo-placements (layer-demo-placement demo))
|
||||||
|
(layer-demo-selection demo)))
|
||||||
|
(_ (layer-demo-status demo))))
|
||||||
|
|
||||||
|
(defun layer-demo-action (demo action)
|
||||||
|
"Create a native keymap for DEMO's business ACTION."
|
||||||
|
(ebox-keymap-create
|
||||||
|
:activate
|
||||||
|
(lambda ()
|
||||||
|
(ebox-call-with-update-batch
|
||||||
|
(lambda ()
|
||||||
|
(pcase action
|
||||||
|
('explode
|
||||||
|
(setf (layer-demo-exploded demo) (not (layer-demo-exploded demo)))
|
||||||
|
(let ((expanded (layer-demo-exploded demo)))
|
||||||
|
(ebox-region-update "orbit-violet"
|
||||||
|
:left (if expanded '(ch 2) '(ch 4))
|
||||||
|
:top (if expanded '(lh 1) '(lh 3)))
|
||||||
|
(ebox-region-update "orbit-signal"
|
||||||
|
:left (if expanded '(ch 26) '(ch 20))
|
||||||
|
:top (if expanded '(lh 11) '(lh 7)))
|
||||||
|
(ebox-region-update "orbit-lime"
|
||||||
|
:left (if expanded '(ch 52) '(ch 46))
|
||||||
|
:top (if expanded '(lh 0) '(lh 2)))))
|
||||||
|
('focus
|
||||||
|
(setf (layer-demo-focus demo) (% (1+ (layer-demo-focus demo)) 3))
|
||||||
|
(cl-loop for id in '("orbit-violet" "orbit-lime" "orbit-signal")
|
||||||
|
for index from 0
|
||||||
|
do (ebox-region-update id :z-index
|
||||||
|
(if (= index (layer-demo-focus demo))
|
||||||
|
6 (+ index 2)))))
|
||||||
|
('mood
|
||||||
|
(setf (layer-demo-warm demo) (not (layer-demo-warm demo)))
|
||||||
|
(cl-loop for id in '("orbit-violet" "orbit-signal" "orbit-lime")
|
||||||
|
for color in (if (layer-demo-warm demo)
|
||||||
|
'("#FFADCF" "#FFD49B" "#B8F1CD")
|
||||||
|
'("#A99BFF" "#B9E9F2" "#D7FA73"))
|
||||||
|
do (ebox-region-update id :background-color color)))
|
||||||
|
('toggle
|
||||||
|
(setf (layer-demo-visible demo) (not (layer-demo-visible demo)))
|
||||||
|
(ebox-region-update "panel-a" :visibility
|
||||||
|
(if (layer-demo-visible demo) 'visible 'hidden)))
|
||||||
|
('move
|
||||||
|
(setf (layer-demo-moved demo) (not (layer-demo-moved demo)))
|
||||||
|
(ebox-region-update "panel-a"
|
||||||
|
:left (if (layer-demo-moved demo) '(ch 10) '(ch 4))
|
||||||
|
:top (if (layer-demo-moved demo) '(lh 0) '(lh 2))))
|
||||||
|
('raise
|
||||||
|
(setf (layer-demo-raised demo) (not (layer-demo-raised demo)))
|
||||||
|
(ebox-region-update "panel-a" :z-index
|
||||||
|
(if (layer-demo-raised demo) 4 2)))
|
||||||
|
('update-lower
|
||||||
|
(cl-incf (layer-demo-revision demo))
|
||||||
|
(ebox-region-update "lower-note" :content
|
||||||
|
(layer-demo-lower-content demo)))
|
||||||
|
('lower
|
||||||
|
(cl-incf (layer-demo-lower-clicks demo))
|
||||||
|
(ebox-region-update "lower-note" :content
|
||||||
|
(layer-demo-lower-content demo)))
|
||||||
|
('menu
|
||||||
|
(setf (layer-demo-menu-visible demo)
|
||||||
|
(not (layer-demo-menu-visible demo)))
|
||||||
|
(ebox-region-update "root-menu" :visibility
|
||||||
|
(if (layer-demo-menu-visible demo)
|
||||||
|
'visible 'hidden)))
|
||||||
|
('placement
|
||||||
|
(setf (layer-demo-placement demo) (% (1+ (layer-demo-placement demo)) 4))
|
||||||
|
(ebox-region-update "root-menu" :placement
|
||||||
|
(aref layer-demo-placements
|
||||||
|
(layer-demo-placement demo))))
|
||||||
|
('anchor
|
||||||
|
(setf (layer-demo-anchor-moved demo)
|
||||||
|
(not (layer-demo-anchor-moved demo)))
|
||||||
|
(ebox-region-update "menu-trigger" :left
|
||||||
|
(if (layer-demo-anchor-moved demo)
|
||||||
|
'(ch 12) '(ch 4))))
|
||||||
|
((or 'sage 'amber)
|
||||||
|
(setf (layer-demo-selection demo) (symbol-name action)
|
||||||
|
(layer-demo-menu-visible demo) nil)
|
||||||
|
(ebox-region-update "menu-result" :content
|
||||||
|
(format "Selected %s from the anchored menu." action)
|
||||||
|
:color (if (eq action 'sage) "#286347" "#895314"))
|
||||||
|
(ebox-region-update "root-menu" :visibility 'hidden))
|
||||||
|
(_ (error "Unknown layer-lab action: %S" action)))
|
||||||
|
(if (memq action '(explode focus mood))
|
||||||
|
(ebox-region-update "orbit-state" :content
|
||||||
|
(layer-demo-orbit-status demo))
|
||||||
|
(ebox-region-update "layer-status" :content
|
||||||
|
(layer-demo-status demo))))))))
|
||||||
|
|
||||||
|
(provide 'layer-demo-reference)
|
||||||
|
|
||||||
|
;;; layer-reference.el ends here
|
||||||
@ -1,64 +0,0 @@
|
|||||||
(column
|
|
||||||
(box :width '(720) :padding '(1 (18)) :border "#8F432F" :color
|
|
||||||
"#252A2E" :bgcolor "#FFFDF8" :font-weight 'bold :text-align 'center
|
|
||||||
"Ebox Playground\nPixel-precise layout, composed only from public nodes.")
|
|
||||||
(box :width '(720) :height 1 :bgcolor "#FAF7F0")
|
|
||||||
(box :width '(720) :padding '(0 (16)) :border-top "#C97252"
|
|
||||||
:border-right "#C97252" :border-left "#C97252"
|
|
||||||
:color "#252A2E" :bgcolor "#F1D4C9"
|
|
||||||
:wrap-mode 'word
|
|
||||||
"01 FIXED + FRACTIONAL\nA definite Grid width divides into one exact track and one flexible track.")
|
|
||||||
(grid :width '(720) :grid-template-columns '((220) (fr 1)) :gap
|
|
||||||
'(1 (12)) :border "#C97252" :bgcolor "#FFF9F5"
|
|
||||||
(box :width '(220) :padding '(1 0) :text-align 'center :color
|
|
||||||
"#252A2E" :bgcolor "#F1D4C9"
|
|
||||||
"FIXED TRACK\n220 px stays exact")
|
|
||||||
(box :width '(486) :padding '(1 0) :text-align 'center :color
|
|
||||||
"#252A2E" :bgcolor "#F9E8E0"
|
|
||||||
"FRACTIONAL TRACK\n1fr receives the remaining width")
|
|
||||||
(box :grid-column '(1 :span 2) :width '(718) :padding '(1 0)
|
|
||||||
:color "#663322" :bgcolor "#FFFDF8" :text-align 'center
|
|
||||||
:wrap-mode 'word
|
|
||||||
"The Grid owns one definite width; its tracks divide that space without competing viewport calculations."))
|
|
||||||
(box :width '(720) :height 1 :bgcolor "#FAF7F0")
|
|
||||||
(box :width '(720) :padding '(0 (16)) :border-top "#6D8A73"
|
|
||||||
:border-right "#6D8A73" :border-left "#6D8A73"
|
|
||||||
:color "#252A2E" :bgcolor "#DCEBDD"
|
|
||||||
:wrap-mode 'word
|
|
||||||
"02 EXPLICIT PLACEMENT\nSpan columns, target cells, and leave the remaining nodes to auto placement.")
|
|
||||||
(grid :width '(720) :grid-template-columns '((210) (210) (fr 1)) :gap
|
|
||||||
'(1 (10)) :border "#6D8A73" :bgcolor "#F7FBF7"
|
|
||||||
(box :grid-column '(1 :span 2) :width '(430) :padding '(1 0)
|
|
||||||
:text-align 'center :color "#24422D" :bgcolor "#DCEBDD"
|
|
||||||
"SPAN 1 → 3")
|
|
||||||
(box :grid-column 3 :grid-row 1 :width '(278) :padding '(1 0)
|
|
||||||
:text-align 'center :color "#24422D" :bgcolor "#C8DDCB"
|
|
||||||
"COLUMN 3")
|
|
||||||
(box :grid-column 1 :grid-row 2 :width '(210) :padding '(1 0)
|
|
||||||
:text-align 'center :color "#24422D" :bgcolor "#EDF5EE"
|
|
||||||
"ROW 2 / COL 1")
|
|
||||||
(box :width '(210) :padding '(1 0) :text-align 'center :color
|
|
||||||
"#24422D" :bgcolor "#E3F0E5" "AUTO PLACED")
|
|
||||||
(box :width '(278) :padding '(1 0) :text-align 'center :color
|
|
||||||
"#24422D" :bgcolor "#D4E6D7" "STABLE CELLS"))
|
|
||||||
(box :width '(720) :height 1 :bgcolor "#FAF7F0")
|
|
||||||
(box :width '(720) :padding '(0 (16)) :border-top "#4E7890"
|
|
||||||
:border-right "#4E7890" :border-left "#4E7890"
|
|
||||||
:color "#252A2E" :bgcolor "#D9EAF2"
|
|
||||||
:wrap-mode 'word
|
|
||||||
"03 PUBLIC COMPOSITION\nColumn, Flex, and Grid share the same public node contract.")
|
|
||||||
(flex :width '(720) :flex-flow '(row wrap) :gap '(1 (12)) :padding
|
|
||||||
'(1 (12)) :border "#4E7890" :bgcolor "#F7FAFC"
|
|
||||||
(box :flex-grow 1 :flex-shrink 1 :flex-basis '(200) :min-width
|
|
||||||
'(180) :padding '(1 (12)) :color "#193846" :bgcolor
|
|
||||||
"#D9EAF2" "COLUMN\nvertical composition")
|
|
||||||
(box :flex-grow 1 :flex-shrink 1 :flex-basis '(200) :min-width
|
|
||||||
'(180) :padding '(1 (12)) :color "#3F3655" :bgcolor
|
|
||||||
"#E7E2F1" "FLEX\nwrap and distribute")
|
|
||||||
(box :flex-grow 1 :flex-shrink 1 :flex-basis '(200) :min-width
|
|
||||||
'(180) :padding '(1 (12)) :color "#6B3020" :bgcolor
|
|
||||||
"#F1D4C9" "GRID\nplace in two dimensions"))
|
|
||||||
(box :width '(720) :height 1 :bgcolor "#FAF7F0")
|
|
||||||
(box :width '(720) :padding '(0 (16)) :border "#8D887F" :color
|
|
||||||
"#4D5651" :bgcolor "#EEEAE2" :wrap-mode 'word
|
|
||||||
"AUTHOR FORMS String · text · box · row · column · flex · grid"))
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
(column :margin-left 1 :margin-top 1 :padding '(1 2 1 2) :bgcolor
|
|
||||||
"linen"
|
|
||||||
(box :padding '(1 (18)) :border "#334155" :text-align 'center
|
|
||||||
:color "#000" :bgcolor "ghost white"
|
|
||||||
"Ebox Responsive Layout\nDrag the preview window: sections stretch to the current viewport, cards reflow, and text wraps without changing the window layout.")
|
|
||||||
(box :height 1)
|
|
||||||
(box :padding '(0 (16)) :border "#0F766E" :bgcolor "#F0FDFA"
|
|
||||||
:color "#134E4A" :wrap-mode 'word
|
|
||||||
"No explicit viewport width here\nThis page mostly omits :width. The Playground binds the current preview width while rendering, so auto width behaves like a responsive container.")
|
|
||||||
(box :height 1)
|
|
||||||
(box :padding '(0 (16)) :border-top "#EA580C"
|
|
||||||
:border-right "#EA580C" :border-left "#EA580C"
|
|
||||||
:bgcolor "#FFF7ED" :color "#431407" :wrap-mode 'word
|
|
||||||
"Adaptive toolbar\nButtons below keep stable sizes. The row packs as many as fit and then wraps.")
|
|
||||||
(flex :flex-flow '(row wrap) :column-gap '(8) :row-gap 1
|
|
||||||
:padding '(1 (8)) :border "#FDBA74" :bgcolor "#FFFBF5"
|
|
||||||
(box :width '(96) :padding '(0 (8)) :border "#EA580C"
|
|
||||||
:bgcolor "#FED7AA" :color "#111827" :text-align
|
|
||||||
'center "Render")
|
|
||||||
(box :width '(88) :padding '(0 (8)) :border "#D97706"
|
|
||||||
:bgcolor "#FEF3C7" :color "#111827" :text-align
|
|
||||||
'center "Save")
|
|
||||||
(box :width '(96) :padding '(0 (8)) :border "#16A34A"
|
|
||||||
:bgcolor "#DCFCE7" :color "#14532D" :text-align
|
|
||||||
'center "Format")
|
|
||||||
(box :width '(104) :padding '(0 (8)) :border "#0284C7"
|
|
||||||
:bgcolor "#E0F2FE" :color "#0C4A6E" :text-align
|
|
||||||
'center "Sample")
|
|
||||||
(box :width '(88) :padding '(0 (8)) :border "#9333EA"
|
|
||||||
:bgcolor "#F3E8FF" :color "#3B0764" :text-align
|
|
||||||
'center "Write"))
|
|
||||||
(box :height 1)
|
|
||||||
(box :padding '(0 (16)) :border-top "#2563EB"
|
|
||||||
:border-right "#2563EB" :border-left "#2563EB"
|
|
||||||
:bgcolor "#EFF6FF" :color "#1E3A8A" :wrap-mode 'word
|
|
||||||
"Responsive cards\nEach card starts from a flex-basis, grows when space is available, and wraps to new rows when the preview becomes narrow.")
|
|
||||||
(flex :flex-flow '(row wrap) :column-gap '(12) :row-gap 1
|
|
||||||
:padding '(1 (8)) :border "#93C5FD" :bgcolor "#F8FBFF"
|
|
||||||
:justify-content 'center
|
|
||||||
(box :flex '(1 1 (210)) :min-width '(180) :max-width
|
|
||||||
'(280) :padding '(0 (10)) :bgcolor "#DBEAFE" :color
|
|
||||||
"#111827" :wrap-mode 'word
|
|
||||||
"Overview\nThe row uses the current preview width automatically.")
|
|
||||||
(box :flex '(1 1 (210)) :min-width '(180) :max-width
|
|
||||||
'(300) :padding '(0 (10)) :bgcolor "#E0E7FF" :color
|
|
||||||
"#111827" :wrap-mode 'word
|
|
||||||
"Editor\nDrag slowly or quickly; resize rendering follows the viewport.")
|
|
||||||
(box :flex '(1 1 (210)) :min-width '(180) :max-width
|
|
||||||
'(320) :padding '(0 (10)) :bgcolor "#EDE9FE" :color
|
|
||||||
"#111827" :wrap-mode 'word
|
|
||||||
"Preview\nThe preview updates from the current viewport width.")
|
|
||||||
(box :flex '(1 1 (210)) :min-width '(180) :max-width
|
|
||||||
'(280) :padding '(0 (10)) :bgcolor "#CFFAFE" :color
|
|
||||||
"#164E63" :wrap-mode 'word
|
|
||||||
"Flow\nCards keep readable widths instead of overflowing."))
|
|
||||||
(box :height 1)
|
|
||||||
(box :padding '(0 (16)) :border "#475569" :bgcolor "#F8FAFC"
|
|
||||||
:color "#0F172A" :wrap-mode 'word
|
|
||||||
"What to watch\nNarrow preview: cards stack and text wraps. Wide preview: cards share a row and expand up to max-width. The source/preview split is preserved."))
|
|
||||||
433
examples/size-reference.ebox
Normal file
433
examples/size-reference.ebox
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
'(column :id "size-root" :width (vw 100) :height (vh 100) :overflow scroll
|
||||||
|
:bgcolor "#FAF7F0" :color "#253038"
|
||||||
|
(box :padding-inline (px 6) :border "#2E3740" :bgcolor "#FFFDF8"
|
||||||
|
:font-weight bold :text-align center "Ebox Size Reference")
|
||||||
|
(box :padding-inline (px 6) :border-left "#2E3740"
|
||||||
|
:border-right "#2E3740" :border-bottom "#2E3740" :bgcolor "#FFFDF8"
|
||||||
|
:text-align center "Lengths, sizing keywords, and calculations.\nHorizontal: px/ch/vw/%. Vertical: lh/vh/%.\nRead the solid rectangle: its edges show the measured Box width and height, including empty space.\nWhite bordered cards hold labels; they are not the measured Box.")
|
||||||
|
(box :height (lh 1))
|
||||||
|
|
||||||
|
;; Each regular card has 220 px of content and a one-pixel border.
|
||||||
|
;; Stretch aligns outer frames; the colored targets retain their own sizes.
|
||||||
|
(box :padding-inline (px 6) :border "#B85C3C" :bgcolor "#F1D4C9"
|
||||||
|
"01 / LENGTH UNITS\nEach card has a 220 px measuring surface.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#B85C3C" :border-right "#B85C3C"
|
||||||
|
:border-bottom "#B85C3C" :bgcolor "#FFF9F4"
|
||||||
|
(column :width (px 222) :border "#B85C3C" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||||
|
":width (px 180)\nPX / fixed pixels")
|
||||||
|
(box :id "size-px" :width (px 180) :height (lh 2)
|
||||||
|
:bgcolor "#D97757" :text-align center "180 px")
|
||||||
|
(box "Fixed width: 180 px."))
|
||||||
|
(column :id "size-percent-parent" :width (px 222)
|
||||||
|
:border "#B85C3C" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||||
|
":width (% 50)\n% / parent content")
|
||||||
|
(box :id "size-percent" :width (% 50) :height (lh 2)
|
||||||
|
:bgcolor "#D97757" :text-align center "110 px")
|
||||||
|
(box "50% of 220 px = 110 px."))
|
||||||
|
(column :id "size-vw-parent" :width (px 222)
|
||||||
|
:border "#B85C3C" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||||
|
":width (vw 20)\n:max-width (% 100)")
|
||||||
|
(box :id "size-vw" :width (vw 20) :max-width (% 100) :height (lh 2)
|
||||||
|
:bgcolor "#D97757" :text-align center "20 vw")
|
||||||
|
(box "20% of viewport width.\nCapped at 220 px."))
|
||||||
|
(column :width (px 222) :border "#B85C3C" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||||
|
":width (ch 12)\n:max-width (% 100)")
|
||||||
|
(box :id "size-ch" :width (ch 12) :max-width (% 100) :height (lh 2)
|
||||||
|
:bgcolor "#D97757" :text-align center "000000000000")
|
||||||
|
(box "Twelve zero advances.\nCapped at 220 px."))
|
||||||
|
(column :width (px 222) :border "#B85C3C" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||||
|
":height (lh 2)\nLH / line count")
|
||||||
|
(box :id "size-lh" :height (lh 2) :bgcolor "#D97757"
|
||||||
|
:text-align center "line one\nline two")
|
||||||
|
(box "Two lines in this font."))
|
||||||
|
(column :width (px 222) :border "#B85C3C" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#B85C3C" :bgcolor "#F7C8B3" :text-align center
|
||||||
|
":height (vh 10)\nVH / viewport height")
|
||||||
|
(box :id "size-vh" :height (vh 10) :bgcolor "#D97757"
|
||||||
|
:text-align center "10 vh")
|
||||||
|
(box "10% of viewport lines,\nfloored to whole lines.")))
|
||||||
|
(box :height (lh 1))
|
||||||
|
|
||||||
|
(box :padding-inline (px 6) :border "#5E7F6A" :bgcolor "#DCEBDD"
|
||||||
|
"02 / FONT AND HEIGHT CONTEXTS\nOnly the measured text changes font. Percentage height needs a definite parent.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#5E7F6A" :border-right "#5E7F6A"
|
||||||
|
:border-bottom "#5E7F6A" :bgcolor "#F3FAF4"
|
||||||
|
(column :id "size-font-small-context" :width (px 222)
|
||||||
|
:border "#5E7F6A" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#5E7F6A" :bgcolor "#BFD6C2" :text-align center
|
||||||
|
":font-size 14\n:width (ch 8)\n:height (lh 2)")
|
||||||
|
(box :id "size-font-small" :width (ch 8) :height (lh 2)
|
||||||
|
:font-size 14 :bgcolor "#8FAF95" "00000000\nsmall")
|
||||||
|
(box "Eight zeros, two lines."))
|
||||||
|
(column :id "size-font-large-context" :width (px 222)
|
||||||
|
:border "#5E7F6A" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#5E7F6A" :bgcolor "#BFD6C2" :text-align center
|
||||||
|
":font-size 22\n:width (ch 8)\n:height (lh 2)")
|
||||||
|
(box :id "size-font-large" :width (ch 8) :height (lh 2)
|
||||||
|
:font-size 22 :bgcolor "#8FAF95" "00000000\nlarge")
|
||||||
|
(box "Same units, larger font."))
|
||||||
|
(column :width (px 222) :border "#5E7F6A" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#5E7F6A" :bgcolor "#BFD6C2" :text-align center
|
||||||
|
":height (% 50)\nParent: :height (lh 6)\nResult: three lines")
|
||||||
|
(column :id "size-percent-height-parent" :height (lh 6)
|
||||||
|
(box :id "size-percent-height" :height (% 50) :bgcolor "#8FAF95"
|
||||||
|
:text-align center "50%\nof six lines"))
|
||||||
|
(box "50% of six = three lines.")))
|
||||||
|
(box :height (lh 1))
|
||||||
|
|
||||||
|
;; Short text shows equivalent results; the constrained cases below
|
||||||
|
;; separate intrinsic sizes using the same text and available width.
|
||||||
|
(box :padding-inline (px 6) :border "#3F6D88" :bgcolor "#CAE6F0"
|
||||||
|
"03 / WIDTH KEYWORDS\nStart with red ink in 112 px frames, then constrain longer text to reveal the differences.")
|
||||||
|
(box :padding-inline (px 6) :border-left "#3F6D88" :border-right "#3F6D88"
|
||||||
|
:bgcolor "#F6FBFD" "WIDTH / auto = stretch in a Column. Short text fits, so fit-content = max-content here.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#3F6D88" :border-right "#3F6D88" :border-bottom "#3F6D88" :bgcolor "#F6FBFD"
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":width\nauto")
|
||||||
|
(box :id "size-width-auto" :width auto :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":width\nmin-content")
|
||||||
|
(box :id "size-width-min-content" :width min-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":width\nmax-content")
|
||||||
|
(box :id "size-width-max-content" :width max-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":width\nfit-content")
|
||||||
|
(box :id "size-width-fit-content" :width fit-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":width\nstretch")
|
||||||
|
(box :id "size-width-stretch" :width stretch :height (lh 2) :bgcolor "#7DB8CE" "red ink")))
|
||||||
|
(box :padding-inline (px 6) :border-left "#3F6D88" :border-right "#3F6D88"
|
||||||
|
:bgcolor "#F6FBFD" "MIN-WIDTH / :width (px 24). auto adds no floor; content keywords raise it; stretch reaches the parent.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#3F6D88" :border-right "#3F6D88" :border-bottom "#3F6D88" :bgcolor "#F6FBFD"
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":min-width\nauto")
|
||||||
|
(box :id "size-min-width-auto" :width (px 24) :min-width auto :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":min-width\nmin-content")
|
||||||
|
(box :id "size-min-width-min-content" :width (px 24) :min-width min-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":min-width\nmax-content")
|
||||||
|
(box :id "size-min-width-max-content" :width (px 24) :min-width max-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":min-width\nfit-content")
|
||||||
|
(box :id "size-min-width-fit-content" :width (px 24) :min-width fit-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":min-width\nstretch")
|
||||||
|
(box :id "size-min-width-stretch" :width (px 24) :min-width stretch :height (lh 2) :bgcolor "#7DB8CE" "red ink")))
|
||||||
|
(box :padding-inline (px 6) :border-left "#3F6D88" :border-right "#3F6D88"
|
||||||
|
:bgcolor "#F6FBFD" "MAX-WIDTH / :width (px 112). none adds no cap; stretch also gives 112 px in this parent.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#3F6D88" :border-right "#3F6D88" :border-bottom "#3F6D88" :bgcolor "#F6FBFD"
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":max-width\nnone")
|
||||||
|
(box :id "size-max-width-none" :width (px 112) :max-width none :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":max-width\nmin-content")
|
||||||
|
(box :id "size-max-width-min-content" :width (px 112) :max-width min-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":max-width\nmax-content")
|
||||||
|
(box :id "size-max-width-max-content" :width (px 112) :max-width max-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":max-width\nfit-content")
|
||||||
|
(box :id "size-max-width-fit-content" :width (px 112) :max-width fit-content :height (lh 2) :bgcolor "#7DB8CE" "red ink"))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center ":max-width\nstretch")
|
||||||
|
(box :id "size-max-width-stretch" :width (px 112) :max-width stretch :height (lh 2) :bgcolor "#7DB8CE" "red ink")))
|
||||||
|
(box :padding-inline (px 6) :border-left "#3F6D88" :border-right "#3F6D88"
|
||||||
|
:bgcolor "#F6FBFD"
|
||||||
|
"LONG CONTENT / Same text, same 8ch parent in each row. Pale ruler = parent width; blue rectangle = actual Box.\nmin-content follows the longest word; fit-content uses 8ch; max-content extends beyond the ruler.\nEach blue rectangle is four lines high. Blank blue space is part of the Box, not extra text.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#3F6D88" :border-right "#3F6D88" :border-bottom "#3F6D88" :bgcolor "#F6FBFD"
|
||||||
|
(column :width (px 222) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center "min-content\nLongest word")
|
||||||
|
(box "width min-content")
|
||||||
|
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||||
|
(column :width (ch 8)
|
||||||
|
(box :id "size-width-long-min-content" :width min-content :height (lh 4)
|
||||||
|
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||||
|
(box "width 1ch + min-width")
|
||||||
|
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||||
|
(column :width (ch 8)
|
||||||
|
(box :id "size-min-width-long-min-content" :width (ch 1) :min-width min-content :height (lh 4)
|
||||||
|
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||||
|
(box "width 20ch + max-width")
|
||||||
|
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||||
|
(column :width (ch 8)
|
||||||
|
(box :id "size-max-width-long-min-content" :width (ch 20) :max-width min-content :height (lh 4)
|
||||||
|
:bgcolor "#7DB8CE" "red ink on blue")))
|
||||||
|
(column :width (px 222) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center "fit-content\nFit available width")
|
||||||
|
(box "width fit-content")
|
||||||
|
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||||
|
(column :width (ch 8)
|
||||||
|
(box :id "size-width-long-fit-content" :width fit-content :height (lh 4)
|
||||||
|
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||||
|
(box "width 1ch + min-width")
|
||||||
|
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||||
|
(column :width (ch 8)
|
||||||
|
(box :id "size-min-width-long-fit-content" :width (ch 1) :min-width fit-content :height (lh 4)
|
||||||
|
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||||
|
(box "width 20ch + max-width")
|
||||||
|
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||||
|
(column :width (ch 8)
|
||||||
|
(box :id "size-max-width-long-fit-content" :width (ch 20) :max-width fit-content :height (lh 4)
|
||||||
|
:bgcolor "#7DB8CE" "red ink on blue")))
|
||||||
|
(column :width (px 222) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center "max-content\nWhole unwrapped text")
|
||||||
|
(box "width max-content")
|
||||||
|
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||||
|
(column :width (ch 8)
|
||||||
|
(box :id "size-width-long-max-content" :width max-content :height (lh 4)
|
||||||
|
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||||
|
(box "width 1ch + min-width")
|
||||||
|
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||||
|
(column :width (ch 8)
|
||||||
|
(box :id "size-min-width-long-max-content" :width (ch 1) :min-width max-content :height (lh 4)
|
||||||
|
:bgcolor "#7DB8CE" "red ink on blue"))
|
||||||
|
(box "width 20ch + max-width")
|
||||||
|
(box :width (ch 8) :bgcolor "#CAE6F0" "8ch")
|
||||||
|
(column :width (ch 8)
|
||||||
|
(box :id "size-max-width-long-max-content" :width (ch 20) :max-width max-content :height (lh 4)
|
||||||
|
:bgcolor "#7DB8CE" "red ink on blue"))))
|
||||||
|
(box :padding-inline (px 6) :border-left "#3F6D88" :border-right "#3F6D88"
|
||||||
|
:bgcolor "#F6FBFD"
|
||||||
|
"ROW CONTEXT / Same 112 px available. auto follows content in a Row; stretch fills the width.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#3F6D88" :border-right "#3F6D88" :border-bottom "#3F6D88" :bgcolor "#F6FBFD"
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center "Row child\n:width auto")
|
||||||
|
(row
|
||||||
|
(box :id "size-width-row-auto" :width auto :height (lh 2) :bgcolor "#7DB8CE" "red ink")))
|
||||||
|
(column :width (px 114) :border "#3F6D88" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3F6D88" :bgcolor "#A8D2E2" :text-align center "Row child\n:width stretch")
|
||||||
|
(row
|
||||||
|
(box :id "size-width-row-stretch" :width stretch :height (lh 2) :bgcolor "#7DB8CE" "red ink"))))
|
||||||
|
(box :height (lh 1))
|
||||||
|
|
||||||
|
(box :padding-inline (px 6) :border "#7C6A2F" :bgcolor "#EEE1A8"
|
||||||
|
"04 / HEIGHT KEYWORDS\nFive-line measuring surfaces. Ebox gives min-content, max-content, and fit-content the same natural height.\nThese three keywords intentionally match: three content lines. stretch fills all five parent lines.")
|
||||||
|
(box :padding-inline (px 6) :border-left "#7C6A2F" :border-right "#7C6A2F"
|
||||||
|
:bgcolor "#FFFDF0" "HEIGHT / three text lines, with automatic, content, or stretch sizing.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#7C6A2F" :border-right "#7C6A2F" :border-bottom "#7C6A2F" :bgcolor "#FFFDF0"
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":height\nauto")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-height-auto" :height auto :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":height\nmin-content")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-height-min-content" :height min-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":height\nmax-content")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-height-max-content" :height max-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":height\nfit-content")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-height-fit-content" :height fit-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":height\nstretch")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-height-stretch" :height stretch :bgcolor "#D9C46D" "red\nink\nhere"))))
|
||||||
|
(box :padding-inline (px 6) :border-left "#7C6A2F" :border-right "#7C6A2F"
|
||||||
|
:bgcolor "#FFFDF0" "MIN-HEIGHT / :height (lh 1). auto adds no floor; content floors to three; stretch floors to five.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#7C6A2F" :border-right "#7C6A2F" :border-bottom "#7C6A2F" :bgcolor "#FFFDF0"
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":min-height\nauto")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-min-height-auto" :height (lh 1) :min-height auto :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":min-height\nmin-content")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-min-height-min-content" :height (lh 1) :min-height min-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":min-height\nmax-content")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-min-height-max-content" :height (lh 1) :min-height max-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":min-height\nfit-content")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-min-height-fit-content" :height (lh 1) :min-height fit-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":min-height\nstretch")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-min-height-stretch" :height (lh 1) :min-height stretch :bgcolor "#D9C46D" "red\nink\nhere"))))
|
||||||
|
(box :padding-inline (px 6) :border-left "#7C6A2F" :border-right "#7C6A2F"
|
||||||
|
:bgcolor "#FFFDF0" "MAX-HEIGHT / :height (lh 5). Content caps to three; none and stretch both allow five here.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#7C6A2F" :border-right "#7C6A2F" :border-bottom "#7C6A2F" :bgcolor "#FFFDF0"
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":max-height\nnone")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-max-height-none" :height (lh 5) :max-height none :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":max-height\nmin-content")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-max-height-min-content" :height (lh 5) :max-height min-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":max-height\nmax-content")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-max-height-max-content" :height (lh 5) :max-height max-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":max-height\nfit-content")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-max-height-fit-content" :height (lh 5) :max-height fit-content :bgcolor "#D9C46D" "red\nink\nhere")))
|
||||||
|
(column :width (px 114) :border "#7C6A2F" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#7C6A2F" :bgcolor "#EEE1A8" :text-align center ":max-height\nstretch")
|
||||||
|
(column :height (lh 5)
|
||||||
|
(box :id "size-max-height-stretch" :height (lh 5) :max-height stretch :bgcolor "#D9C46D" "red\nink\nhere"))))
|
||||||
|
(box :height (lh 1))
|
||||||
|
|
||||||
|
(box :padding-inline (px 6) :border "#6F6A95" :bgcolor "#E4DEF4"
|
||||||
|
"05 / SIZE FUNCTIONS\nEvery operand follows the property's axis. The white frames have 220 px of content.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#6F6A95" :border-right "#6F6A95"
|
||||||
|
:border-bottom "#6F6A95" :bgcolor "#FBF9FF"
|
||||||
|
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||||
|
":width (calc (- (% 100) (px 24)))")
|
||||||
|
(box :id "size-calc" :width (calc (- (% 100) (px 24))) :height (lh 2)
|
||||||
|
:bgcolor "#AFA3D1" :text-align center "196 px")
|
||||||
|
(box "220 - 24 = 196 px."))
|
||||||
|
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||||
|
":width (min (% 75) (px 100))")
|
||||||
|
(box :id "size-min" :width (min (% 75) (px 100)) :height (lh 2)
|
||||||
|
:bgcolor "#AFA3D1" :text-align center "100 px")
|
||||||
|
(box "min(165, 100) = 100 px."))
|
||||||
|
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||||
|
":width (max (px 60) (% 50))")
|
||||||
|
(box :id "size-max" :width (max (px 60) (% 50)) :height (lh 2)
|
||||||
|
:bgcolor "#AFA3D1" :text-align center "110 px")
|
||||||
|
(box "max(60, 110) = 110 px."))
|
||||||
|
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||||
|
":width (clamp (px 48) (vw 12) (px 200))")
|
||||||
|
(box :id "size-clamp" :width (clamp (px 48) (vw 12) (px 200)) :height (lh 2)
|
||||||
|
:bgcolor "#AFA3D1" :text-align center "clamp")
|
||||||
|
(box "12vw, bounded to 48..200px."))
|
||||||
|
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||||
|
":width (calc (/ (* (+ (px 24) (px 16)) 3) 2))")
|
||||||
|
(box :id "size-arithmetic" :width (calc (/ (* (+ (px 24) (px 16)) 3) 2))
|
||||||
|
:height (lh 2) :bgcolor "#AFA3D1" :text-align center "60 px")
|
||||||
|
(box "(24 + 16) x 3 / 2 = 60 px."))
|
||||||
|
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center
|
||||||
|
":width (min (% 100) (max (px 80) (vw 20)))")
|
||||||
|
(box :id "size-nested" :width (min (% 100) (max (px 80) (vw 20)))
|
||||||
|
:height (lh 2) :bgcolor "#AFA3D1" :text-align center "nested")
|
||||||
|
(box "20vw, bounded to 80..220px.")))
|
||||||
|
(box :padding-inline (px 6) :border-left "#6F6A95" :border-right "#6F6A95"
|
||||||
|
:bgcolor "#FBF9FF"
|
||||||
|
"COMPARE FUNCTIONS / Same input X in each card; fixed bounds 48..144 px.\nmin caps the top; max floors the bottom; clamp does both. Compare all three input ranges at once.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#6F6A95" :border-right "#6F6A95" :border-bottom "#6F6A95" :bgcolor "#FBF9FF"
|
||||||
|
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center "LOW / X = 24 px\nBelow the 48 px floor")
|
||||||
|
(box "min(X, 144) = 24 px")
|
||||||
|
(box :id "size-function-min-low" :width (min (px 24) (px 144)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||||
|
(box "max(48, X) = 48 px")
|
||||||
|
(box :id "size-function-max-low" :width (max (px 48) (px 24)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||||
|
(box "clamp(48, X, 144) = 48 px")
|
||||||
|
(box :id "size-function-clamp-low" :width (clamp (px 48) (px 24) (px 144)) :height (lh 1) :bgcolor "#AFA3D1"))
|
||||||
|
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center "MID / X = 96 px\nInside the 48..144 px range")
|
||||||
|
(box "min(X, 144) = 96 px")
|
||||||
|
(box :id "size-function-min-mid" :width (min (px 96) (px 144)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||||
|
(box "max(48, X) = 96 px")
|
||||||
|
(box :id "size-function-max-mid" :width (max (px 48) (px 96)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||||
|
(box "clamp(48, X, 144) = 96 px")
|
||||||
|
(box :id "size-function-clamp-mid" :width (clamp (px 48) (px 96) (px 144)) :height (lh 1) :bgcolor "#AFA3D1"))
|
||||||
|
(column :width (px 222) :border "#6F6A95" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#6F6A95" :bgcolor "#CBC2E3" :text-align center "HIGH / X = 192 px\nAbove the 144 px cap")
|
||||||
|
(box "min(X, 144) = 144 px")
|
||||||
|
(box :id "size-function-min-high" :width (min (px 192) (px 144)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||||
|
(box "max(48, X) = 192 px")
|
||||||
|
(box :id "size-function-max-high" :width (max (px 48) (px 192)) :height (lh 1) :bgcolor "#AFA3D1")
|
||||||
|
(box "clamp(48, X, 144) = 144 px")
|
||||||
|
(box :id "size-function-clamp-high" :width (clamp (px 48) (px 192) (px 144)) :height (lh 1) :bgcolor "#AFA3D1")))
|
||||||
|
(box :height (lh 1))
|
||||||
|
|
||||||
|
(box :padding-inline (px 6) :border "#3D7068" :bgcolor "#CDEBE5"
|
||||||
|
"06 / BOX MODEL, FLEX, AND GRID\nThe same axis rules size edges, gaps, bases, and tracks.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#3D7068" :border-right "#3D7068"
|
||||||
|
:border-bottom "#3D7068" :bgcolor "#F7FCFB"
|
||||||
|
(column :width (px 222) :border "#3D7068" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3D7068" :bgcolor "#CDEBE5" :text-align center
|
||||||
|
":box-sizing border-box\n:padding ((lh 1) (ch 1))")
|
||||||
|
(box :id "size-box-model" :width (px 180) :height (lh 4) :box-sizing border-box
|
||||||
|
:padding ((lh 1) (ch 1)) :margin ((lh 0) (px 6))
|
||||||
|
:border ((px 1) solid "#3D7068") :bgcolor "#9FD5CB"
|
||||||
|
"padding\ninside the border")
|
||||||
|
(box "180px outer; 6px margins.\n1lh/1ch padding; 1px stroke."))
|
||||||
|
(column :width (px 222) :border "#3D7068" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3D7068" :bgcolor "#CDEBE5" :text-align center
|
||||||
|
":flex (1 1 (ch 12))\n:flex (2 1 (px 96))")
|
||||||
|
(flex :id "size-flex" :flex-wrap wrap :gap ((lh 1) (px 10))
|
||||||
|
(box :id "size-flex-basis" :flex (1 1 (ch 12)) :min-width (px 0)
|
||||||
|
:height (lh 2) :bgcolor "#9FD5CB" :text-align center "1 share\n12ch")
|
||||||
|
(box :flex (2 1 (px 96)) :min-width (px 0) :height (lh 2)
|
||||||
|
:bgcolor "#71BDB0" :text-align center "2 shares\n96px"))
|
||||||
|
(box "Bases: 12ch and 96px.\nLarger fonts may wrap items.\nGap: 1lh x 10px."))
|
||||||
|
(column :width (px 222) :border "#3D7068" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3D7068" :bgcolor "#CDEBE5" :text-align center
|
||||||
|
":flex (1 1 (px 30))\n:flex (2 1 (px 30))")
|
||||||
|
(flex :id "size-flex-grow" :column-gap (px 10)
|
||||||
|
(box :id "size-flex-grow-one" :flex (1 1 (px 30)) :min-width (px 0)
|
||||||
|
:height (lh 2) :bgcolor "#9FD5CB" :text-align center "80px\n+50px")
|
||||||
|
(box :id "size-flex-grow-two" :flex (2 1 (px 30)) :min-width (px 0)
|
||||||
|
:height (lh 2) :bgcolor "#71BDB0" :text-align center "130px\n+100px"))
|
||||||
|
(box "Same 30px base; 10px gap.\n150px extra splits 1:2.\nTotal widths: 80px / 130px."))
|
||||||
|
(column :width (px 222) :border "#3D7068" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#3D7068" :bgcolor "#CDEBE5" :text-align center
|
||||||
|
":grid-template-columns ((ch 8) (fr 1))\n:grid-template-rows ((lh 2) (lh 2))")
|
||||||
|
(grid :id "size-grid" :grid-template-columns ((ch 8) (fr 1))
|
||||||
|
:grid-template-rows ((lh 2) (lh 2)) :gap ((lh 1) (ch 1))
|
||||||
|
(box :bgcolor "#9FD5CB" :text-align center "8ch")
|
||||||
|
(box :bgcolor "#71BDB0" :text-align center "1fr")
|
||||||
|
(box :bgcolor "#9FD5CB" :text-align center "2lh")
|
||||||
|
(box :bgcolor "#71BDB0" :text-align center "2lh"))
|
||||||
|
(box "Two 2lh rows.\nGap: 1lh x 1ch.")))
|
||||||
|
(box :height (lh 1))
|
||||||
|
|
||||||
|
(box :padding-inline (px 6) :border "#9A6B43" :bgcolor "#F1D0AF"
|
||||||
|
"07 / PRECISION AND BOUNDARIES\nHorizontal calculations retain fractions. Final vertical sizes use whole lines.\n2.75lh and 2lh intentionally render alike: both resolve to two whole lines.")
|
||||||
|
(flex :flex-wrap wrap :column-gap (px 6) :padding-inline (px 6)
|
||||||
|
:border-left "#9A6B43" :border-right "#9A6B43"
|
||||||
|
:border-bottom "#9A6B43" :bgcolor "#FFFAF5"
|
||||||
|
(column :width (px 222) :border "#9A6B43" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#9A6B43" :bgcolor "#F1D0AF" :text-align center
|
||||||
|
":width (vw 12)\n:max-width (% 100)")
|
||||||
|
(box :id "size-fractional-width" :width (vw 12) :max-width (% 100)
|
||||||
|
:height (lh 2) :bgcolor "#D9AD83" :text-align center "12 vw")
|
||||||
|
(box "853px viewport: 102.36px.\nCapped at 220px content."))
|
||||||
|
(column :width (px 222) :border "#9A6B43" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#9A6B43" :bgcolor "#F1D0AF" :text-align center
|
||||||
|
":height (lh 2.75)\n:overflow hidden")
|
||||||
|
(box :id "size-fractional-height" :height (lh 2.75) :overflow hidden
|
||||||
|
:bgcolor "#D9AD83" :text-align center "line one\nline two\nline three")
|
||||||
|
(box "Floors to two visible lines.\nThe third line is clipped."))
|
||||||
|
(column :width (px 222) :border "#9A6B43" :bgcolor "#FFFDF8"
|
||||||
|
(box :border-bottom "#9A6B43" :bgcolor "#F1D0AF" :text-align center
|
||||||
|
":height (lh 2)\n:overflow hidden")
|
||||||
|
(box :id "size-whole-height" :height (lh 2) :overflow hidden
|
||||||
|
:bgcolor "#D9AD83" :text-align center "line one\nline two\nline three")
|
||||||
|
(box "Exactly two visible lines.\nThe third line is clipped.")))
|
||||||
|
(box :padding-inline (px 6) :border-left "#9A6B43" :border-right "#9A6B43"
|
||||||
|
:border-bottom "#9A6B43" :bgcolor "#FFFAF5"
|
||||||
|
"Empty auto-sized boxes occupy zero lines; use :height (lh 1) for a spacer.\nVertical properties reject px/ch/vw, including function operands. Border strokes reject percentages."))
|
||||||
@ -56,7 +56,8 @@
|
|||||||
(defconst ebox-playground-flex-resize-evaluator--runtime-properties
|
(defconst ebox-playground-flex-resize-evaluator--runtime-properties
|
||||||
(delete-dups
|
(delete-dups
|
||||||
(append (mapcar #'cdr ebox-region-types)
|
(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.")
|
"Ebox identity properties omitted from fresh-render comparison.")
|
||||||
|
|
||||||
(defun ebox-playground-flex-resize-evaluator--line-widths (buffer)
|
(defun ebox-playground-flex-resize-evaluator--line-widths (buffer)
|
||||||
@ -65,19 +66,175 @@
|
|||||||
(mapcar #'ebox-string-pixel-width
|
(mapcar #'ebox-string-pixel-width
|
||||||
(ebox-string-lines (buffer-string)))))
|
(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)
|
(defun ebox-playground-flex-resize-evaluator--visual-text (text)
|
||||||
"Return TEXT with fresh-render identity properties removed.
|
"Return TEXT with runtime ids removed and face cascades normalized.
|
||||||
Fresh renders allocate different region ids by design. Keep character,
|
Keep characters, display dimensions, effective face attributes, and interactive
|
||||||
face, display, and all non-runtime properties so the comparison still checks
|
properties. Equivalent anonymous face stacks compare equal; changes to visible
|
||||||
the visible right-hand surface rather than merely checking that rendering
|
color, relative font size, measurement, or interaction still compare unequal."
|
||||||
completed."
|
(let ((copy (copy-sequence text))
|
||||||
(let ((copy (copy-sequence text)))
|
(hover-owners (make-hash-table :test #'eql)))
|
||||||
(when (> (length copy) 0)
|
(when (> (length copy) 0)
|
||||||
(remove-list-of-text-properties
|
(remove-list-of-text-properties
|
||||||
0 (length copy)
|
0 (length copy)
|
||||||
ebox-playground-flex-resize-evaluator--runtime-properties
|
ebox-playground-flex-resize-evaluator--runtime-properties
|
||||||
|
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))
|
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))
|
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)
|
(defun ebox-playground-flex-resize-evaluator--check (name condition detail)
|
||||||
"Record a named check and return whether CONDITION passed."
|
"Record a named check and return whether CONDITION passed."
|
||||||
@ -86,6 +243,532 @@ completed."
|
|||||||
ebox-playground-flex-resize-evaluator--output-lines)
|
ebox-playground-flex-resize-evaluator--output-lines)
|
||||||
condition)
|
condition)
|
||||||
|
|
||||||
|
(defun ebox-playground-scroll-evaluator--measure (region-id steps &optional cold)
|
||||||
|
"Measure STEPS one-line scrolls of REGION-ID.
|
||||||
|
Alternate cached motion by default; COLD advances without retracing prior rows."
|
||||||
|
(let ((root-renders 0) (node-styles 0) (style-computations 0)
|
||||||
|
(document-preparations 0) (surface-plans 0)
|
||||||
|
(prefix-renders 0) (materializations 0)
|
||||||
|
(deadline (+ (float-time) 30))
|
||||||
|
samples installed gc-start gc-time-start)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(dolist (entry
|
||||||
|
(list
|
||||||
|
(cons 'ebox-incremental-prepare-scoped-commit
|
||||||
|
(lambda (&rest _) (cl-incf document-preparations)))
|
||||||
|
(cons 'ebox-surface--surface-plan
|
||||||
|
(lambda (&rest _) (cl-incf surface-plans)))
|
||||||
|
(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)))
|
||||||
|
(cons 'ebox--render-scroll-window-source
|
||||||
|
(lambda (&rest _) (cl-incf prefix-renders)))
|
||||||
|
(cons 'ebox--scroll-state-materialize-lines
|
||||||
|
(lambda (_region state)
|
||||||
|
;; The entry point also accepts already-complete
|
||||||
|
;; state; count only actual producer invocations.
|
||||||
|
(when (plist-get state :materialize-content-lines)
|
||||||
|
(cl-incf materializations))))))
|
||||||
|
(advice-add (car entry) :before (cdr entry))
|
||||||
|
(push entry installed))
|
||||||
|
(setq gc-start gcs-done gc-time-start gc-elapsed)
|
||||||
|
(dotimes (index steps)
|
||||||
|
(when (> (float-time) deadline)
|
||||||
|
(error "Scroll evaluator exceeded 30 seconds after %d/%d samples"
|
||||||
|
index steps))
|
||||||
|
(let* ((delta (if (or cold (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)
|
||||||
|
(if cold
|
||||||
|
(error
|
||||||
|
"Cold scroll stopped at sample %d/%d after %d completed steps: moved %S, expected 1 (capacity may be exhausted)"
|
||||||
|
(1+ index) steps index consumed)
|
||||||
|
(error "Scroll evaluator moved %S lines, expected %S"
|
||||||
|
consumed delta)))))
|
||||||
|
(list :gc-count (- gcs-done gc-start)
|
||||||
|
:gc-ms (* 1000.0 (- gc-elapsed gc-time-start))
|
||||||
|
: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
|
||||||
|
:document-preparations document-preparations
|
||||||
|
:surface-plans surface-plans
|
||||||
|
:style-computations style-computations
|
||||||
|
:prefix-renders prefix-renders :materializations materializations))
|
||||||
|
(dolist (entry installed)
|
||||||
|
(advice-remove (car entry) (cdr entry))))))
|
||||||
|
|
||||||
|
(defun ebox-playground-scroll-evaluator--fresh-parity (buffer file)
|
||||||
|
"Compare BUFFER with its committed input at every committed scroll offset.
|
||||||
|
FILE is retained for caller compatibility; the oracle never reloads source or
|
||||||
|
recreates the companion's business state, callbacks, or keymaps."
|
||||||
|
(ignore file)
|
||||||
|
(let* ((state (ebox--buffer-render-state buffer))
|
||||||
|
(snapshot (ebox-surface-buffer-snapshot buffer))
|
||||||
|
(input (plist-get snapshot :input))
|
||||||
|
(ebox-viewport-width (plist-get state :viewport-width))
|
||||||
|
(ebox-viewport-height (plist-get state :viewport-height))
|
||||||
|
(published-root (plist-get state :root-node))
|
||||||
|
(offsets (make-hash-table :test #'eql))
|
||||||
|
(source-ids (make-hash-table :test #'eq))
|
||||||
|
(rendered-owners (make-hash-table :test #'eql))
|
||||||
|
rendered-root
|
||||||
|
(set-offsets
|
||||||
|
(lambda (root)
|
||||||
|
(unless rendered-root
|
||||||
|
(when (eq root published-root)
|
||||||
|
(error "Fresh scroll oracle reached the published root"))
|
||||||
|
(setq rendered-root root)
|
||||||
|
;; Snapshots omit runtime node IDs. Preserved opaque source
|
||||||
|
;; handles bridge each committed ID to its detached counterpart.
|
||||||
|
(cl-labels
|
||||||
|
((visit (node)
|
||||||
|
(when-let* ((node-id (gethash (ebox-node-source-handle node)
|
||||||
|
source-ids)))
|
||||||
|
(when (gethash node-id rendered-owners)
|
||||||
|
(error "Fresh scroll oracle found duplicate owner %s" node-id))
|
||||||
|
(ebox-put node :scroll-offset (gethash node-id offsets))
|
||||||
|
(ebox-put node :ebox-scroll-offset-controlled-p t)
|
||||||
|
(puthash node-id node rendered-owners))
|
||||||
|
(ebox-tree-for-each-direct-child node #'visit)))
|
||||||
|
(visit root)))))
|
||||||
|
expected)
|
||||||
|
(maphash
|
||||||
|
(lambda (region scroll-state)
|
||||||
|
(let* ((node-id (ebox-runtime-index-get
|
||||||
|
region (plist-get state :region-node-table)))
|
||||||
|
(node (and node-id (ebox-runtime-index-get
|
||||||
|
node-id (plist-get state :node-table))))
|
||||||
|
(source (and node (ebox-node-source-handle node)))
|
||||||
|
(offset (plist-get scroll-state :scroll-offset)))
|
||||||
|
(unless (and source (integerp offset) (>= offset 0)
|
||||||
|
(equal offset (ebox-get node :scroll-offset)))
|
||||||
|
(error "Fresh scroll oracle requires a consistent committed offset for region %s"
|
||||||
|
region))
|
||||||
|
(when (gethash source source-ids)
|
||||||
|
(error "Fresh scroll oracle found ambiguous source for region %s" region))
|
||||||
|
(puthash node-id offset offsets)
|
||||||
|
(puthash source node-id source-ids)))
|
||||||
|
(plist-get state :scroll-state-table))
|
||||||
|
;; 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-offsets)
|
||||||
|
(setq expected (ebox-render input))
|
||||||
|
(unless (and rendered-root
|
||||||
|
(= (hash-table-count offsets)
|
||||||
|
(hash-table-count rendered-owners)))
|
||||||
|
(error "Fresh scroll oracle did not reach every committed scroll owner"))
|
||||||
|
(maphash
|
||||||
|
(lambda (node-id offset)
|
||||||
|
(unless (equal offset (ebox-get (gethash node-id rendered-owners)
|
||||||
|
:scroll-offset))
|
||||||
|
(error "Fresh scroll oracle changed offset for owner %s" node-id)))
|
||||||
|
offsets))
|
||||||
|
(advice-remove 'ebox--render-layout set-offsets))
|
||||||
|
(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
|
||||||
|
(ebox-playground-scroll-evaluator--report-mismatch actual fresh))
|
||||||
|
same))))
|
||||||
|
|
||||||
|
(defun ebox-playground-scroll-evaluator--report-mismatch (actual fresh)
|
||||||
|
"Print bounded differences between ACTUAL and FRESH without opaque values.
|
||||||
|
Report character codes, changed property names, line widths, and face summaries;
|
||||||
|
never print callback environments or keymap contents."
|
||||||
|
(let* ((limit (min (length actual) (length fresh)))
|
||||||
|
(position
|
||||||
|
(or (cl-loop for index below limit
|
||||||
|
unless (equal-including-properties
|
||||||
|
(substring actual index (1+ index))
|
||||||
|
(substring fresh index (1+ index)))
|
||||||
|
return index)
|
||||||
|
limit))
|
||||||
|
(left (and (< position (length actual))
|
||||||
|
(text-properties-at position actual)))
|
||||||
|
(right (and (< position (length fresh))
|
||||||
|
(text-properties-at position fresh)))
|
||||||
|
(keys (delete-dups
|
||||||
|
(append (cl-loop for (key _value) on left by #'cddr collect key)
|
||||||
|
(cl-loop for (key _value) on right by #'cddr collect key))))
|
||||||
|
(changed (cl-remove-if
|
||||||
|
(lambda (key) (equal (plist-get left key) (plist-get right key)))
|
||||||
|
keys)))
|
||||||
|
(cl-labels
|
||||||
|
((bounded (value)
|
||||||
|
(let* ((print-level 3) (print-length 8) (print-circle t)
|
||||||
|
(print-escape-newlines t)
|
||||||
|
(text (prin1-to-string value)))
|
||||||
|
(if (> (length text) 160) (concat (substring text 0 157) "...") text)))
|
||||||
|
(line-width (text)
|
||||||
|
(let ((start (or (cl-position ?\n text :end position :from-end t) -1))
|
||||||
|
(end (or (cl-position ?\n text :start position) (length text))))
|
||||||
|
(ebox-string-pixel-width (substring text (1+ start) end)))))
|
||||||
|
(princ
|
||||||
|
(format
|
||||||
|
" render lengths=%d/%d first-difference=%d char-codes=%S/%S changed-properties=%s line-widths=%s/%s faces=%s -> %s\n"
|
||||||
|
(length actual) (length fresh) position
|
||||||
|
(and (< position (length actual)) (aref actual position))
|
||||||
|
(and (< position (length fresh)) (aref fresh position))
|
||||||
|
(bounded changed) (line-width actual) (line-width fresh)
|
||||||
|
(bounded (plist-get left 'face)) (bounded (plist-get right 'face)))))))
|
||||||
|
|
||||||
|
(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 :full-surface-renders 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--render-candidate . :full-surface-renders)
|
||||||
|
(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--max-offset (state)
|
||||||
|
"Return STATE's final maximum offset, or nil for an incomplete lazy prefix."
|
||||||
|
(when (or (plist-get state :content-lines-complete-p)
|
||||||
|
(not (or (plist-get state :render-content-prefix)
|
||||||
|
(plist-get state :materialize-content-lines))))
|
||||||
|
(max 0 (- (length (plist-get state :content-lines))
|
||||||
|
(plist-get state :content-height)))))
|
||||||
|
|
||||||
|
(defun ebox-playground-scroll-evaluator-run
|
||||||
|
(files &optional width steps height options)
|
||||||
|
"Profile scrolling for FILES at WIDTH with STEPS, HEIGHT, and OPTIONS.
|
||||||
|
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.
|
||||||
|
OPTIONS accepts :region-id, an author semantic ID resolved in each mount,
|
||||||
|
and :cold, either t or nil. An omitted or nil ID selects the root. Runtime
|
||||||
|
numeric region metadata is not accepted as an address; integer IDs mean author
|
||||||
|
IDs. The selected node must be scrollable.
|
||||||
|
|
||||||
|
By default fully materialize selected content outside the samples, then measure
|
||||||
|
cached top and midpoint motion. With :cold t, measure only forward one-line
|
||||||
|
motion from the initial mounted offset, with no explicit prewarming,
|
||||||
|
materialization, or seek. Signal an error identifying the failed sample if
|
||||||
|
capacity runs out before STEPS completes; do not report incomplete success.
|
||||||
|
|
||||||
|
Return bounded plists with :mode, semantic :region-id, :start-offset,
|
||||||
|
:end-offset, :content-complete-p, timings, full root render counts, node style
|
||||||
|
visits, style computations, and fresh-render parity for each position.
|
||||||
|
:prefix-renders counts calls to the bounded scroll source renderer;
|
||||||
|
:materializations counts full-content producer invocations during samples.
|
||||||
|
:document-preparations counts general scoped document preparation;
|
||||||
|
:surface-plans counts construction of full surface publication plans.
|
||||||
|
:position is top/middle for cached samples or cold for advancing samples.
|
||||||
|
:max-offset is nil unless the selected content is fully materialized.
|
||||||
|
The oracle independently renders the committed snapshot, preserving opaque
|
||||||
|
callbacks and keymaps, and reapplies every committed scroll owner's offset.
|
||||||
|
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.
|
||||||
|
Each sample loop checks a 30-second deadline between motions."
|
||||||
|
(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"))
|
||||||
|
(unless (and (proper-list-p options) (zerop (% (length options) 2)))
|
||||||
|
(error "Scroll evaluator OPTIONS must be an even property list"))
|
||||||
|
(let (seen)
|
||||||
|
(cl-loop for (key _value) on options by #'cddr do
|
||||||
|
(unless (and (memq key '(:region-id :cold)) (not (memq key seen)))
|
||||||
|
(error "Scroll evaluator OPTIONS has an unknown or repeated key"))
|
||||||
|
(push key seen)))
|
||||||
|
(unless (and (memq (plist-get options :cold) '(nil t))
|
||||||
|
(let ((id (plist-get options :region-id)))
|
||||||
|
(or (null id) (stringp id) (symbolp id) (integerp id))))
|
||||||
|
(error "Scroll evaluator needs a semantic :region-id and boolean :cold"))
|
||||||
|
(dolist (value (list width steps height))
|
||||||
|
(unless (and (integerp value) (> value 0))
|
||||||
|
(error "Scroll evaluator dimensions and steps must be positive integers")))
|
||||||
|
(let ((selected-id (plist-get options :region-id))
|
||||||
|
(cold (plist-get options :cold))
|
||||||
|
(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
|
||||||
|
(if selected-id
|
||||||
|
(cdr (ebox-selector--region-target
|
||||||
|
(ebox-region-resolve buffer selected-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 selected owner: %s"
|
||||||
|
file))
|
||||||
|
(unless cold
|
||||||
|
(setq state (ebox--scroll-state-materialize-lines
|
||||||
|
region-id state))
|
||||||
|
(puthash region-id state table)
|
||||||
|
(setq max-offset
|
||||||
|
(ebox-playground-scroll-evaluator--max-offset state))
|
||||||
|
(unless max-offset
|
||||||
|
(error "Scroll evaluator could not materialize selected content"))
|
||||||
|
(unless (> max-offset 0)
|
||||||
|
(error "Scroll evaluator selected owner 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 (if cold '(cold) '(top middle)))
|
||||||
|
(unless cold
|
||||||
|
(ebox--surface-scroll-to-offset
|
||||||
|
buffer region-id
|
||||||
|
(if (eq position 'top) 0 (/ max-offset 2))))
|
||||||
|
(let* ((start-offset
|
||||||
|
(plist-get (ebox--scroll-get-state region-id)
|
||||||
|
:scroll-offset))
|
||||||
|
(measurement
|
||||||
|
(ebox-playground-scroll-evaluator--measure
|
||||||
|
region-id steps cold))
|
||||||
|
(final-state (ebox--scroll-get-state region-id))
|
||||||
|
(final-max-offset
|
||||||
|
(ebox-playground-scroll-evaluator--max-offset final-state))
|
||||||
|
(parity
|
||||||
|
(ebox-playground-scroll-evaluator--fresh-parity
|
||||||
|
buffer file))
|
||||||
|
(result
|
||||||
|
(append
|
||||||
|
(list :file (expand-file-name file)
|
||||||
|
:width width :height height :steps steps
|
||||||
|
:region-id selected-id :mode (if cold 'cold 'cached)
|
||||||
|
:position position
|
||||||
|
:start-offset start-offset
|
||||||
|
:end-offset (plist-get final-state :scroll-offset)
|
||||||
|
:content-complete-p (numberp final-max-offset)
|
||||||
|
:max-offset final-max-offset
|
||||||
|
:fresh-render-parity parity)
|
||||||
|
measurement)))
|
||||||
|
(princ
|
||||||
|
(format
|
||||||
|
"%s scroll %s %s mean=%.3fms p95=%.3fms max=%.3fms gc=%d/%.3fms roots=%d document-preparations=%d surface-plans=%d node-styles=%d style-computations=%d prefix-renders=%d materializations=%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 :gc-count)
|
||||||
|
(plist-get measurement :gc-ms)
|
||||||
|
(plist-get measurement :root-renders)
|
||||||
|
(plist-get measurement :document-preparations)
|
||||||
|
(plist-get measurement :surface-plans)
|
||||||
|
(plist-get measurement :node-styles)
|
||||||
|
(plist-get measurement :style-computations)
|
||||||
|
(plist-get measurement :prefix-renders)
|
||||||
|
(plist-get measurement :materializations) 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
|
(defun ebox-playground-flex-resize-evaluator--settle-preview
|
||||||
(preview preview-window)
|
(preview preview-window)
|
||||||
"Publish PREVIEW at its settled PREVIEW-WINDOW viewport before measuring.
|
"Publish PREVIEW at its settled PREVIEW-WINDOW viewport before measuring.
|
||||||
|
|||||||
580
tests/ebox-playground-desktop-tests.el
Normal file
580
tests/ebox-playground-desktop-tests.el
Normal file
@ -0,0 +1,580 @@
|
|||||||
|
;;; ebox-playground-desktop-tests.el --- Desktop interaction regressions -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Drive the real example through rendered native commands. Drag streams are
|
||||||
|
;; deterministic Emacs events; every visible intermediate position is checked
|
||||||
|
;; with the existing committed-snapshot render oracle.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ebox-playground)
|
||||||
|
(require 'ebox-playground-layer-tests
|
||||||
|
(expand-file-name "tests/ebox-playground-layer-tests.el"
|
||||||
|
ebox-playground-directory))
|
||||||
|
|
||||||
|
(defconst ebox-playground-desktop-test--file
|
||||||
|
(expand-file-name "examples/desktop-reference.ebox" ebox-playground-directory)
|
||||||
|
"Desktop example exercised through the ordinary file runner.")
|
||||||
|
|
||||||
|
(defvar ebox-playground-desktop-test--demo nil
|
||||||
|
"Business state captured from this test's real preview construction.")
|
||||||
|
|
||||||
|
(defvar ebox-playground-desktop-test--redisplays 0
|
||||||
|
"Redisplay requests issued during the current synthetic drag.")
|
||||||
|
|
||||||
|
(cl-defmacro ebox-playground-desktop-test--with-preview
|
||||||
|
((buffer &optional (width 120) (height 36)) &rest body)
|
||||||
|
"Evaluate BODY in an independent BUFFER preview of WIDTH and HEIGHT pixels."
|
||||||
|
(declare (indent 1) (debug ((symbolp &optional form form) body)))
|
||||||
|
`(let* ((,buffer (generate-new-buffer " *ebox-desktop-test*"))
|
||||||
|
(ebox-viewport-width ,width) (ebox-viewport-height ,height)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil)
|
||||||
|
(ebox-playground-desktop-test--demo nil)
|
||||||
|
(capture (lambda (demo)
|
||||||
|
(setq ebox-playground-desktop-test--demo demo))))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(advice-add 'desktop-demo-create :filter-return capture)
|
||||||
|
(ebox-playground-open-file
|
||||||
|
ebox-playground-desktop-test--file (buffer-name ,buffer))
|
||||||
|
(with-current-buffer ,buffer
|
||||||
|
(should ebox-playground-desktop-test--demo)
|
||||||
|
,@body))
|
||||||
|
(advice-remove 'desktop-demo-create capture)
|
||||||
|
(when (buffer-live-p ,buffer)
|
||||||
|
(ebox-playground-close (buffer-name ,buffer))))))
|
||||||
|
|
||||||
|
(defun ebox-playground-desktop-test--window (id)
|
||||||
|
"Return the captured business window identified by ID."
|
||||||
|
(cl-find id (desktop-demo-windows ebox-playground-desktop-test--demo)
|
||||||
|
:key #'desktop-demo-window-id :test #'equal))
|
||||||
|
|
||||||
|
(defun ebox-playground-desktop-test--offset (id)
|
||||||
|
"Return ID's committed pixel displacement."
|
||||||
|
(let ((window (ebox-playground-desktop-test--window id)))
|
||||||
|
(cons (desktop-demo-window-x window) (desktop-demo-window-y window))))
|
||||||
|
|
||||||
|
(defun ebox-playground-desktop-test--revision ()
|
||||||
|
"Return the current preview's committed publication revision."
|
||||||
|
(plist-get (ebox-surface-buffer-snapshot (current-buffer)) :revision))
|
||||||
|
|
||||||
|
(defun ebox-playground-desktop-test--state ()
|
||||||
|
"Copy the business values which must advance only after publication."
|
||||||
|
(let ((demo ebox-playground-desktop-test--demo))
|
||||||
|
(list (copy-sequence (desktop-demo-order demo))
|
||||||
|
(desktop-demo-active demo) (desktop-demo-serial demo)
|
||||||
|
(desktop-demo-dragging demo)
|
||||||
|
(desktop-demo-theme demo)
|
||||||
|
(mapcar (lambda (window)
|
||||||
|
(list (desktop-demo-window-id window)
|
||||||
|
(desktop-demo-window-x window)
|
||||||
|
(desktop-demo-window-y window)))
|
||||||
|
(desktop-demo-windows demo)))))
|
||||||
|
|
||||||
|
(defun ebox-playground-desktop-test--event (type window position xy &optional end)
|
||||||
|
"Create native TYPE at WINDOW POSITION XY, optionally ending at END."
|
||||||
|
(let ((start (list window position xy 0)))
|
||||||
|
(if end (list type start (list window position end 1))
|
||||||
|
(list type start))))
|
||||||
|
|
||||||
|
(defun ebox-playground-desktop-test--drag (label start events &optional type)
|
||||||
|
"Drag LABEL from START, reading EVENTS produced for its window and position.
|
||||||
|
EVENTS is called with the window and position. Each returned item is an event
|
||||||
|
or a function run before reading the next event, to inspect live publication.
|
||||||
|
TYPE defaults to `down-mouse-1'; repeated presses use their native event type."
|
||||||
|
(let* ((buffer (current-buffer))
|
||||||
|
(position (ebox-playground-layer-test--position label))
|
||||||
|
(map (get-text-property position 'keymap)))
|
||||||
|
(save-window-excursion
|
||||||
|
(set-window-buffer (selected-window) buffer)
|
||||||
|
(let* ((window (selected-window))
|
||||||
|
(down (ebox-playground-desktop-test--event
|
||||||
|
(or type 'down-mouse-1) window position start))
|
||||||
|
(queue (funcall events window position))
|
||||||
|
(command (key-binding (vector down) nil nil position))
|
||||||
|
(last-input-event down)
|
||||||
|
(ebox-playground-desktop-test--redisplays 0)
|
||||||
|
(unread-command-events nil))
|
||||||
|
(should (commandp command))
|
||||||
|
(should (eq command (lookup-key map (vector (or type 'down-mouse-1)))))
|
||||||
|
(cl-letf (((symbol-function 'read-event)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(while (functionp (car queue)) (funcall (pop queue)))
|
||||||
|
(unless queue (ert-fail "Drag read beyond its terminal event"))
|
||||||
|
(pop queue)))
|
||||||
|
((symbol-function 'redisplay)
|
||||||
|
(lambda (&rest _) (cl-incf ebox-playground-desktop-test--redisplays))))
|
||||||
|
(call-interactively command nil (vector down)))
|
||||||
|
(should-not queue)
|
||||||
|
(should-not (desktop-demo-dragging ebox-playground-desktop-test--demo))
|
||||||
|
(ebox-playground-layer-test--parity)
|
||||||
|
unread-command-events))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-renders-five-independent-window-owners ()
|
||||||
|
"The example retains five window owners and visible dock controls at two widths."
|
||||||
|
(dolist (width '(80 120))
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer width)
|
||||||
|
(should (= (length (desktop-demo-windows ebox-playground-desktop-test--demo)) 5))
|
||||||
|
(dolist (id '("desk-studio" "desk-notes" "desk-signal" "desk-terminal" "desk-palette"))
|
||||||
|
(should (ebox-region-resolve buffer id)))
|
||||||
|
(dolist (label '("01 ST" "02 NT" "03 SG" "04 TM" "05 PL" "RESET" "CYCLE"))
|
||||||
|
(should (keymapp (get-text-property
|
||||||
|
(ebox-playground-layer-test--position label) 'keymap))))
|
||||||
|
(dolist (line (ebox-string-lines (buffer-string)))
|
||||||
|
(should (<= (ebox-string-pixel-width line) width)))
|
||||||
|
(ebox-playground-layer-test--parity))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-compact-viewport-keeps-all-dock-commands ()
|
||||||
|
"Every dock control remains visible and usable in a 40 by 20 viewport."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer 40 20)
|
||||||
|
(dolist (entry '(("01 ST" . "desk-studio") ("02 NT" . "desk-notes")
|
||||||
|
("03 SG" . "desk-signal") ("04 TM" . "desk-terminal")
|
||||||
|
("05 PL" . "desk-palette")))
|
||||||
|
(ebox-playground-layer-test--click (car entry))
|
||||||
|
(should (equal (desktop-demo-active ebox-playground-desktop-test--demo)
|
||||||
|
(cdr entry))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-gaps-and-endpoints-do-not-select-text ()
|
||||||
|
"Desktop gaps, newlines and buffer end consume native selection commands."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(should desktop-demo-input-mode)
|
||||||
|
(let* ((dock (ebox-playground-layer-test--position "01 ST"))
|
||||||
|
(gap (next-single-property-change dock 'keymap nil (point-max)))
|
||||||
|
(newline (save-excursion (goto-char dock) (line-end-position)))
|
||||||
|
(transient-mark-mode t))
|
||||||
|
(save-window-excursion
|
||||||
|
(set-window-buffer (selected-window) buffer)
|
||||||
|
(dolist (position (list (point-min) gap newline (point-max)))
|
||||||
|
(dolist (type '(down-mouse-1 mouse-1 drag-mouse-1 S-mouse-1
|
||||||
|
M-down-mouse-1 M-mouse-1 M-drag-mouse-1))
|
||||||
|
(goto-char position)
|
||||||
|
(set-mark (if (= position (point-min)) (point-max) (point-min)))
|
||||||
|
(setq mark-active t)
|
||||||
|
(let* ((event (ebox-playground-desktop-test--event
|
||||||
|
type (selected-window) position '(0 . 0)))
|
||||||
|
(command (key-binding (vector type)))
|
||||||
|
(last-input-event event))
|
||||||
|
(should (eq command #'desktop-demo--dismiss-mouse-selection))
|
||||||
|
(call-interactively command)
|
||||||
|
(should-not mark-active))))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-fast-dock-clicks-remain-controls ()
|
||||||
|
"Single, double and triple dock click sequences never activate a region."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(let ((transient-mark-mode t))
|
||||||
|
(save-window-excursion
|
||||||
|
(set-window-buffer (selected-window) buffer)
|
||||||
|
(dolist (label '("01 ST" "02 NT" "03 SG" "04 TM" "05 PL"))
|
||||||
|
(dolist (type '(down-mouse-1 mouse-1 double-down-mouse-1 double-mouse-1
|
||||||
|
triple-down-mouse-1 triple-mouse-1 drag-mouse-1
|
||||||
|
double-drag-mouse-1 triple-drag-mouse-1))
|
||||||
|
(let* ((position (ebox-playground-layer-test--position label))
|
||||||
|
(event (ebox-playground-desktop-test--event
|
||||||
|
type (selected-window) position '(0 . 0)))
|
||||||
|
(last-input-event event))
|
||||||
|
(goto-char position)
|
||||||
|
(set-mark (point-min))
|
||||||
|
(setq mark-active t)
|
||||||
|
(run-hooks 'pre-command-hook)
|
||||||
|
(let ((command (key-binding (vector type))))
|
||||||
|
(should (commandp command))
|
||||||
|
(should-not (memq command '(mouse-drag-region mouse-set-region mouse-set-point)))
|
||||||
|
(call-interactively command))
|
||||||
|
(should-not mark-active))))))
|
||||||
|
(ebox-playground-layer-test--parity)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-mouse-policy-is-local-and-released ()
|
||||||
|
"Other buffers and replacement previews retain ordinary Emacs selection."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(with-temp-buffer
|
||||||
|
(should-not desktop-demo-input-mode)
|
||||||
|
(should (eq (key-binding [down-mouse-1]) 'mouse-drag-region)))
|
||||||
|
(ebox-playground-open-file ebox-playground-layer-test--file (buffer-name buffer))
|
||||||
|
(should-not desktop-demo-input-mode)
|
||||||
|
(should-not (memq #'desktop-demo--clear-mouse-mark pre-command-hook))
|
||||||
|
(ebox-playground-open-file ebox-playground-desktop-test--file (buffer-name buffer))
|
||||||
|
(should desktop-demo-input-mode)
|
||||||
|
(ebox-unmount-buffer buffer)
|
||||||
|
(should-not desktop-demo-input-mode)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-theme-adapts-all-card-and-chrome-styles ()
|
||||||
|
"A theme toggle changes every semantic paint role without moving windows."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(let* ((demo ebox-playground-desktop-test--demo)
|
||||||
|
(before (ebox-playground-desktop-test--state))
|
||||||
|
(revision (ebox-playground-desktop-test--revision)))
|
||||||
|
(ebox-playground-layer-test--activate "LIGHT")
|
||||||
|
(should (eq (desktop-demo-theme demo) 'light))
|
||||||
|
(should (= (ebox-playground-desktop-test--revision) (1+ revision)))
|
||||||
|
(dolist (entry (desktop-demo-styles demo))
|
||||||
|
(let ((matches (ebox-selector-query-buffer
|
||||||
|
buffer (concat "." (symbol-name (car entry))))))
|
||||||
|
(should matches)
|
||||||
|
(unless (eq (car entry) 'desktop-chrome)
|
||||||
|
(dolist (match matches)
|
||||||
|
(let ((node (plist-get match :node)) (style (nth 2 entry)))
|
||||||
|
(while style
|
||||||
|
(let ((property (pop style)) (expected (pop style)))
|
||||||
|
(unless (eq property :hover-style)
|
||||||
|
(should (equal (ebox-get node (if (eq property :background-color)
|
||||||
|
:bgcolor property))
|
||||||
|
expected))))))))))
|
||||||
|
(should (string-match-p "DARK" (buffer-string)))
|
||||||
|
(should (member "#B9C9DE"
|
||||||
|
(flatten-tree (get-text-property
|
||||||
|
(ebox-playground-layer-test--position "DARK") 'mouse-face))))
|
||||||
|
(ebox-playground-layer-test--activate "DARK")
|
||||||
|
(should (equal before (ebox-playground-desktop-test--state)))
|
||||||
|
(should (eq (desktop-demo-theme demo) 'dark)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-light-theme-keeps-focus-move-and-reset ()
|
||||||
|
"Light cards and active chrome stay light after normal window operations."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer 40 20)
|
||||||
|
(ebox-playground-layer-test--activate "LIGHT")
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(let* ((demo ebox-playground-desktop-test--demo)
|
||||||
|
(position (ebox-playground-layer-test--position "01 ST"))
|
||||||
|
(face (flatten-tree (get-text-property position 'face))))
|
||||||
|
(should (member "#6D4AA2" face))
|
||||||
|
(should (member "#FFFFFF" face))
|
||||||
|
(ebox-playground-layer-test--activate "01 ST" (kbd "M-<right>"))
|
||||||
|
(ebox-playground-layer-test--activate "RESET")
|
||||||
|
(should (eq (desktop-demo-theme demo) 'light))
|
||||||
|
(should (equal (desktop-demo-active demo) "desk-palette"))
|
||||||
|
(should (member "#875270" (flatten-tree
|
||||||
|
(get-text-property
|
||||||
|
(ebox-playground-layer-test--position "05 PL") 'face))))
|
||||||
|
(dolist (label '("01 ST" "02 NT" "03 SG" "04 TM" "05 PL" "DARK"))
|
||||||
|
(should (ebox-playground-layer-test--position label))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-failed-theme-keeps-palette-and-state ()
|
||||||
|
"Rejected theme publication rolls back the complete palette and state."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(dolist (failure-step '(text client-state))
|
||||||
|
(let ((before (buffer-string))
|
||||||
|
(state (ebox-playground-desktop-test--state))
|
||||||
|
(tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step failure-step) (error "Reject theme")))))
|
||||||
|
(should-error (ebox-playground-layer-test--activate "LIGHT"))
|
||||||
|
(should (equal state (ebox-playground-desktop-test--state)))
|
||||||
|
(should (equal-including-properties before (buffer-string)))))
|
||||||
|
(ebox-playground-layer-test--activate "LIGHT")))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-light-palette-has-pale-cards-and-dark-ink ()
|
||||||
|
"Every light card is pale and its body/caption text has useful contrast."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(let ((styles (desktop-demo-styles ebox-playground-desktop-test--demo)))
|
||||||
|
(cl-labels
|
||||||
|
((light (role) (nth 2 (assq role styles)))
|
||||||
|
(luminance (hex)
|
||||||
|
(cl-loop for start in '(1 3 5) for weight in '(0.2126 0.7152 0.0722)
|
||||||
|
for value = (/ (string-to-number (substring hex start (+ start 2)) 16) 255.0)
|
||||||
|
sum (* weight (if (<= value 0.04045) (/ value 12.92)
|
||||||
|
(expt (/ (+ value 0.055) 1.055) 2.4)))))
|
||||||
|
(contrast (fg bg)
|
||||||
|
(let ((a (luminance fg)) (b (luminance bg)))
|
||||||
|
(/ (+ (max a b) 0.05) (+ (min a b) 0.05)))))
|
||||||
|
(dolist (role '(desktop-root desktop-workspace desktop-studio desktop-notes
|
||||||
|
desktop-signal desktop-terminal desktop-palette))
|
||||||
|
(let ((style (light role)))
|
||||||
|
(should (> (luminance (plist-get style :background-color)) 0.7))
|
||||||
|
(when (plist-get style :color)
|
||||||
|
(should (> (contrast (plist-get style :color)
|
||||||
|
(plist-get style :background-color)) 4.5)))))
|
||||||
|
(dolist (pair '((desktop-studio-muted . desktop-studio)
|
||||||
|
(desktop-notes-caption . desktop-notes)
|
||||||
|
(desktop-signal-caption . desktop-signal)
|
||||||
|
(desktop-terminal-muted . desktop-terminal)
|
||||||
|
(desktop-palette-caption . desktop-palette)))
|
||||||
|
(should (> (contrast (plist-get (light (car pair)) :color)
|
||||||
|
(plist-get (light (cdr pair)) :background-color)) 4.5)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-previews-have-independent-themes ()
|
||||||
|
"Changing one desktop palette cannot recolor a second preview."
|
||||||
|
(ebox-playground-desktop-test--with-preview (first)
|
||||||
|
(let ((first-demo ebox-playground-desktop-test--demo))
|
||||||
|
(ebox-playground-desktop-test--with-preview (second)
|
||||||
|
(let ((before (buffer-string)))
|
||||||
|
(with-current-buffer first
|
||||||
|
(desktop-demo-toggle-theme first-demo)
|
||||||
|
(ebox-playground-layer-test--parity))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(should (eq (desktop-demo-theme ebox-playground-desktop-test--demo) 'dark))
|
||||||
|
(should (eq (desktop-demo-theme first-demo) 'light)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-repeated-keyboard-nudges-retain-dock-binding ()
|
||||||
|
"Repeated arrow commands keep their native dock binding without repositioning."
|
||||||
|
(dolist (key '("M-<right>" "M-<left>" "M-<up>" "M-<down>"))
|
||||||
|
(let ((report (ebox-playground-interaction-evaluator-run
|
||||||
|
ebox-playground-desktop-test--file "01 ST" key 3 104 36)))
|
||||||
|
(should (= (plist-get report :steps) 3))
|
||||||
|
(should (>= (plist-get report :publications) 3))
|
||||||
|
(should (plist-get report :fresh-parity)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-repeated-title-presses-still-drag ()
|
||||||
|
"Double and triple title presses track movement using their native bindings."
|
||||||
|
(dolist (types '((double-down-mouse-1 . double-drag-mouse-1)
|
||||||
|
(triple-down-mouse-1 . triple-drag-mouse-1)))
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" '(10 . 8)
|
||||||
|
(lambda (window position)
|
||||||
|
(list (ebox-playground-desktop-test--event
|
||||||
|
'mouse-movement window position '(12 . 9))
|
||||||
|
(ebox-playground-desktop-test--event
|
||||||
|
(cdr types) window position '(10 . 8) '(13 . 10))))
|
||||||
|
(car types))
|
||||||
|
(should (equal (ebox-playground-desktop-test--offset "desk-studio") '(3 . 2))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-native-dock-raises-every-window ()
|
||||||
|
"Every dock command raises its entire window including its nested content."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(dolist (entry '(("01 ST" "desk-studio" "Make space for the unexpected.")
|
||||||
|
("02 NT" "desk-notes" "A quieter kind of focus.")
|
||||||
|
("03 SG" "desk-signal" "AFTER HOURS")
|
||||||
|
("04 TM" "desk-terminal" "$ build something good")
|
||||||
|
("05 PL" "desk-palette" "Objects in conversation.")))
|
||||||
|
(ebox-playground-layer-test--click (car entry))
|
||||||
|
(should (equal (desktop-demo-active ebox-playground-desktop-test--demo)
|
||||||
|
(cadr entry)))
|
||||||
|
(should (integerp (ebox-playground-layer-test--position (caddr entry)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-front-window-click-does-not-publish ()
|
||||||
|
"Repeated activation of the front window is a publication no-op."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(let ((revision (ebox-playground-desktop-test--revision))
|
||||||
|
(state (ebox-playground-desktop-test--state)))
|
||||||
|
(ebox-playground-layer-test--click "Make space for the unexpected.")
|
||||||
|
(should (= revision (ebox-playground-desktop-test--revision)))
|
||||||
|
(should (equal state (ebox-playground-desktop-test--state))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-exposed-body-raises-covered-nested-content ()
|
||||||
|
"Clicking an exposed lower body restores its nested artwork above an overlapping pane."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer 80)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" '(10 . 8)
|
||||||
|
(lambda (window position)
|
||||||
|
(list (ebox-playground-desktop-test--event
|
||||||
|
'drag-mouse-1 window position '(10 . 8) '(28 . 11)))))
|
||||||
|
(let ((artwork (ebox-playground-layer-test--coordinates "PLAY / 03")))
|
||||||
|
(ebox-playground-layer-test--activate "03 SG")
|
||||||
|
(should-not (string-match-p "PLAY / 03" (buffer-string)))
|
||||||
|
(ebox-playground-layer-test--click "Make space for the unexpected.")
|
||||||
|
(should (equal (desktop-demo-active ebox-playground-desktop-test--demo) "desk-studio"))
|
||||||
|
(should (equal artwork (ebox-playground-layer-test--coordinates "PLAY / 03")))
|
||||||
|
(ebox-playground-layer-test--activate "02 NT")
|
||||||
|
(ebox-playground-layer-test--click "PLAY / 03")
|
||||||
|
(should (equal (desktop-demo-active ebox-playground-desktop-test--demo) "desk-studio")))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-drag-publishes-each-motion-before-release ()
|
||||||
|
"Motion publishes immediately and release uses its endpoint, preserving grab offset."
|
||||||
|
(dolist (width '(80 120))
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer width)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" '(10 . 8)
|
||||||
|
(lambda (window position)
|
||||||
|
(list
|
||||||
|
(ebox-playground-desktop-test--event 'mouse-movement window position '(15 . 10))
|
||||||
|
(lambda ()
|
||||||
|
(should (equal (ebox-playground-desktop-test--offset "desk-studio") '(5 . 2)))
|
||||||
|
(should (desktop-demo-dragging ebox-playground-desktop-test--demo))
|
||||||
|
(should (>= ebox-playground-desktop-test--redisplays 2))
|
||||||
|
(ebox-playground-layer-test--parity))
|
||||||
|
(ebox-playground-desktop-test--event 'mouse-movement window position '(12 . 9))
|
||||||
|
(lambda ()
|
||||||
|
(should (equal (ebox-playground-desktop-test--offset "desk-studio") '(2 . 1)))
|
||||||
|
(should (>= ebox-playground-desktop-test--redisplays 3))
|
||||||
|
(ebox-playground-layer-test--parity))
|
||||||
|
(ebox-playground-desktop-test--event 'drag-mouse-1 window position '(10 . 8) '(16 . 11)))))
|
||||||
|
(should (equal (ebox-playground-desktop-test--offset "desk-studio") '(6 . 3))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-repeated-motion-does-not-republish ()
|
||||||
|
"Identical pointer positions and a matching release produce only one publication."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(let ((revision (ebox-playground-desktop-test--revision)))
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" '(10 . 8)
|
||||||
|
(lambda (window position)
|
||||||
|
(list (ebox-playground-desktop-test--event 'mouse-movement window position '(12 . 9))
|
||||||
|
(ebox-playground-desktop-test--event 'mouse-movement window position '(12 . 9))
|
||||||
|
(ebox-playground-desktop-test--event 'drag-mouse-1 window position '(10 . 8) '(12 . 9)))))
|
||||||
|
(should (= (1+ revision) (ebox-playground-desktop-test--revision))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-second-drag-starts-at-current-displacement ()
|
||||||
|
"A second grab adds its delta to the previously published window position."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(dolist (start '((10 . 8) (25 . 16)))
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" start
|
||||||
|
(lambda (window position)
|
||||||
|
(list (ebox-playground-desktop-test--event
|
||||||
|
'drag-mouse-1 window position start
|
||||||
|
(cons (+ (car start) 3) (1+ (cdr start))))))))
|
||||||
|
(should (equal (ebox-playground-desktop-test--offset "desk-studio") '(6 . 2)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-each-title-drags-its-own-window ()
|
||||||
|
"Each title's native drag binding moves its associated semantic owner."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(dolist (entry '(("01 ST" "STUDIO / 01" "desk-studio")
|
||||||
|
("02 NT" "NOTES / 02" "desk-notes")
|
||||||
|
("03 SG" "SIGNAL / 03" "desk-signal")
|
||||||
|
("04 TM" "TERMINAL / 04" "desk-terminal")
|
||||||
|
("05 PL" "PALETTE / 05" "desk-palette")))
|
||||||
|
(ebox-playground-layer-test--activate (car entry))
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
(cadr entry) '(10 . 8)
|
||||||
|
(lambda (window position)
|
||||||
|
(list (ebox-playground-desktop-test--event
|
||||||
|
'drag-mouse-1 window position '(10 . 8) '(11 . 9)))))
|
||||||
|
(should (equal (ebox-playground-desktop-test--offset (caddr entry)) '(1 . 1))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-reverse-drag-restores-original-paint ()
|
||||||
|
"Moving away and back restores original coordinates and committed render parity."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(let ((home (ebox-playground-layer-test--coordinates "STUDIO / 01")))
|
||||||
|
(dolist (end '((17 . 11) (3 . 5)))
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" '(10 . 8)
|
||||||
|
(lambda (window position)
|
||||||
|
(list (ebox-playground-desktop-test--event
|
||||||
|
'drag-mouse-1 window position '(10 . 8) end)))))
|
||||||
|
(should (equal (ebox-playground-desktop-test--offset "desk-studio") '(0 . 0)))
|
||||||
|
(should (equal home (ebox-playground-layer-test--coordinates "STUDIO / 01"))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-unrelated-event-stops-and-is-requeued ()
|
||||||
|
"Typing during a drag ends tracking and preserves the input for the command loop."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(should (equal (ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" '(10 . 8) (lambda (_window _position) (list ?x)))
|
||||||
|
'(120)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-quit-clears-dragging ()
|
||||||
|
"A quit signal exits tracking without leaving the demo in its dragging state."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(let ((caught nil))
|
||||||
|
(condition-case nil
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" '(10 . 8)
|
||||||
|
(lambda (_window _position) (list (lambda () (signal 'quit nil)))))
|
||||||
|
(quit (setq caught t)))
|
||||||
|
(should caught)
|
||||||
|
(should-not (desktop-demo-dragging ebox-playground-desktop-test--demo))
|
||||||
|
(ebox-playground-layer-test--parity))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-failed-activation-preserves-business-state ()
|
||||||
|
"A rejected publication leaves focus, ordering and all window positions unchanged."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(ebox-playground-layer-test--activate "05 PL")
|
||||||
|
(let ((state (ebox-playground-desktop-test--state))
|
||||||
|
(revision (ebox-playground-desktop-test--revision)))
|
||||||
|
(dolist (failure-step '(text client-state))
|
||||||
|
(let* ((failed nil)
|
||||||
|
(tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step failure-step)
|
||||||
|
(setq failed t)
|
||||||
|
(error "Injected desktop publication failure: %s" step)))))
|
||||||
|
(should-error (ebox-playground-layer-test--activate "01 ST"))
|
||||||
|
(should failed)))
|
||||||
|
(should (equal state (ebox-playground-desktop-test--state)))
|
||||||
|
(should (= revision (ebox-playground-desktop-test--revision)))
|
||||||
|
(ebox-playground-layer-test--parity))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-failed-motion-keeps-last-accepted-position ()
|
||||||
|
"A rejected live drag publication keeps geometry and clears transient tracking."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(let* ((revision (ebox-playground-desktop-test--revision))
|
||||||
|
(failed nil)
|
||||||
|
(tp--surface-publication-step-function
|
||||||
|
(lambda (step _surface)
|
||||||
|
(when (eq step 'client-state)
|
||||||
|
(setq failed t)
|
||||||
|
(error "Reject drag position")))))
|
||||||
|
(should-error
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" '(10 . 8)
|
||||||
|
(lambda (window position)
|
||||||
|
(list (ebox-playground-desktop-test--event
|
||||||
|
'mouse-movement window position '(15 . 10))))))
|
||||||
|
(should failed)
|
||||||
|
(should-not (desktop-demo-dragging ebox-playground-desktop-test--demo))
|
||||||
|
(should (equal (ebox-playground-desktop-test--offset "desk-studio") '(0 . 0)))
|
||||||
|
(should (= revision (ebox-playground-desktop-test--revision))))
|
||||||
|
(ebox-playground-layer-test--parity)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-outside-release-ends-tracking ()
|
||||||
|
"Release over another window or frame area consumes the event and ends tracking."
|
||||||
|
(dolist (target '(window frame area))
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(let ((other (generate-new-buffer " *ebox-desktop-outside*")))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(with-current-buffer other (insert "Untouched other window"))
|
||||||
|
(should-not
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" '(10 . 8)
|
||||||
|
(lambda (window position)
|
||||||
|
(let* ((other-window (split-window-right))
|
||||||
|
(where (if (eq target 'frame) (selected-frame) other-window)))
|
||||||
|
(set-window-buffer other-window other)
|
||||||
|
(list (list 'drag-mouse-1
|
||||||
|
(list window position '(10 . 8) 0)
|
||||||
|
(list where (if (eq target 'area) 'mode-line 1)
|
||||||
|
'(500 . 500) 1)))))))
|
||||||
|
(with-current-buffer other
|
||||||
|
(should (equal (buffer-string) "Untouched other window"))))
|
||||||
|
(kill-buffer other))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-reset-restores-all-window-positions ()
|
||||||
|
"Reset returns dragged windows to their original position and focus order."
|
||||||
|
(ebox-playground-desktop-test--with-preview (buffer)
|
||||||
|
(let ((initial-order (copy-sequence (desktop-demo-order ebox-playground-desktop-test--demo)))
|
||||||
|
(initial-active (desktop-demo-active ebox-playground-desktop-test--demo)))
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" '(10 . 8)
|
||||||
|
(lambda (window position)
|
||||||
|
(list (ebox-playground-desktop-test--event
|
||||||
|
'drag-mouse-1 window position '(10 . 8) '(16 . 10)))))
|
||||||
|
(ebox-playground-layer-test--activate "RESET")
|
||||||
|
(dolist (window (desktop-demo-windows ebox-playground-desktop-test--demo))
|
||||||
|
(should (equal (ebox-playground-desktop-test--offset (desktop-demo-window-id window))
|
||||||
|
'(0 . 0))))
|
||||||
|
(should (equal initial-order (desktop-demo-order ebox-playground-desktop-test--demo)))
|
||||||
|
(should (equal initial-active (desktop-demo-active ebox-playground-desktop-test--demo))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-desktop-previews-keep-independent-focus-and-position ()
|
||||||
|
"A second preview starts fresh while the first retains its focus and displacement."
|
||||||
|
(ebox-playground-desktop-test--with-preview (first 80)
|
||||||
|
(ebox-playground-layer-test--activate "01 ST")
|
||||||
|
(ebox-playground-desktop-test--drag
|
||||||
|
"STUDIO / 01" '(10 . 8)
|
||||||
|
(lambda (window position)
|
||||||
|
(list (ebox-playground-desktop-test--event
|
||||||
|
'drag-mouse-1 window position '(10 . 8) '(14 . 10)))))
|
||||||
|
(let ((first-demo ebox-playground-desktop-test--demo)
|
||||||
|
(state (ebox-playground-desktop-test--state)))
|
||||||
|
(ebox-playground-desktop-test--with-preview (second 120)
|
||||||
|
(should-not (eq first-demo ebox-playground-desktop-test--demo))
|
||||||
|
(should (equal (ebox-playground-desktop-test--offset "desk-studio") '(0 . 0)))
|
||||||
|
(ebox-playground-layer-test--activate "CYCLE")
|
||||||
|
(with-current-buffer first
|
||||||
|
(let ((ebox-playground-desktop-test--demo first-demo))
|
||||||
|
(should (equal state (ebox-playground-desktop-test--state)))
|
||||||
|
(ebox-playground-layer-test--parity)))))))
|
||||||
|
|
||||||
|
(provide 'ebox-playground-desktop-tests)
|
||||||
|
;;; ebox-playground-desktop-tests.el ends here
|
||||||
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
|
||||||
397
tests/ebox-playground-layer-tests.el
Normal file
397
tests/ebox-playground-layer-tests.el
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
;;; ebox-playground-layer-tests.el --- Layer lab command regressions -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Exercise the actual companion and DSL through the generic file runner.
|
||||||
|
;; Commands come from rendered native keymaps. Every publication is compared
|
||||||
|
;; with an independent render of the committed logical input, including paint
|
||||||
|
;; and native interaction properties after normalizing runtime identities.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ebox-playground)
|
||||||
|
(require 'ebox-playground-flex-resize-evaluator
|
||||||
|
(expand-file-name "scripts/ebox-playground-flex-resize-evaluator.el"
|
||||||
|
ebox-playground-directory))
|
||||||
|
|
||||||
|
(defconst ebox-playground-layer-test--file
|
||||||
|
(expand-file-name "examples/layer-reference.ebox" ebox-playground-directory)
|
||||||
|
"Layer lab loaded by this suite through the ordinary file runner.")
|
||||||
|
|
||||||
|
(cl-defmacro ebox-playground-layer-test--with-preview
|
||||||
|
((buffer &optional (width 720)) &rest body)
|
||||||
|
"Run BODY in a fresh BUFFER preview of WIDTH pixels and clean up afterward."
|
||||||
|
(declare (indent 1) (debug ((symbolp &optional form) body)))
|
||||||
|
`(let ((,buffer (generate-new-buffer " *ebox-layer-test*"))
|
||||||
|
(ebox-viewport-width ,width) (ebox-viewport-height 100)
|
||||||
|
(case-fold-search nil)
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ebox-playground-open-file
|
||||||
|
ebox-playground-layer-test--file (buffer-name ,buffer))
|
||||||
|
(with-current-buffer ,buffer ,@body))
|
||||||
|
(when (buffer-live-p ,buffer)
|
||||||
|
(ebox-playground-close (buffer-name ,buffer))))))
|
||||||
|
|
||||||
|
(defun ebox-playground-layer-test--position (label)
|
||||||
|
"Return the first visible position of LABEL in the current preview."
|
||||||
|
(save-excursion
|
||||||
|
(goto-char (point-min))
|
||||||
|
(unless (search-forward label nil t)
|
||||||
|
(ert-fail (format "Missing visible layer label: %s" label)))
|
||||||
|
(- (point) (length label))))
|
||||||
|
|
||||||
|
(defun ebox-playground-layer-test--coordinates (label)
|
||||||
|
"Return LABEL's visible horizontal pixel and zero-based row coordinates."
|
||||||
|
(save-excursion
|
||||||
|
(goto-char (ebox-playground-layer-test--position label))
|
||||||
|
(cons (ebox-string-pixel-width
|
||||||
|
(buffer-substring (line-beginning-position) (point)))
|
||||||
|
(1- (line-number-at-pos)))))
|
||||||
|
|
||||||
|
(defun ebox-playground-layer-test--parity ()
|
||||||
|
"Compare native paint and interactions with the committed snapshot render."
|
||||||
|
(let* ((state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(ebox-viewport-width (plist-get state :viewport-width))
|
||||||
|
(ebox-viewport-height (plist-get state :viewport-height))
|
||||||
|
(snapshot (ebox-surface-buffer-snapshot (current-buffer)))
|
||||||
|
(fresh (ebox-render (plist-get snapshot :input))))
|
||||||
|
(should (ebox-canonical-input-p (plist-get snapshot :input)))
|
||||||
|
(should
|
||||||
|
(equal-including-properties
|
||||||
|
(ebox-playground-scroll-evaluator--visual-text (buffer-string))
|
||||||
|
(ebox-playground-scroll-evaluator--visual-text fresh)))))
|
||||||
|
|
||||||
|
(defun ebox-playground-layer-test--activate (label &optional key)
|
||||||
|
"Invoke the native command at LABEL using KEY, then verify its publication."
|
||||||
|
(let* ((keys (or key (kbd "RET")))
|
||||||
|
(position (ebox-playground-layer-test--position label))
|
||||||
|
(map (get-text-property position 'keymap)))
|
||||||
|
(goto-char position)
|
||||||
|
(should (keymapp map))
|
||||||
|
(let ((command (key-binding keys))
|
||||||
|
(last-input-event (aref (vconcat keys) (1- (length keys)))))
|
||||||
|
(should (commandp command))
|
||||||
|
(should (eq command (lookup-key map keys)))
|
||||||
|
(call-interactively command)
|
||||||
|
(ebox-playground-layer-test--parity)
|
||||||
|
command)))
|
||||||
|
|
||||||
|
(defun ebox-playground-layer-test--click (label)
|
||||||
|
"Dispatch LABEL's native mouse command while another buffer is current."
|
||||||
|
(let* ((buffer (current-buffer))
|
||||||
|
(position (ebox-playground-layer-test--position label))
|
||||||
|
(map (get-text-property position 'keymap)))
|
||||||
|
(save-window-excursion
|
||||||
|
(set-window-buffer (selected-window) buffer)
|
||||||
|
(let* ((event (list 'mouse-1 (list (selected-window) position '(0 . 0) 0)))
|
||||||
|
(last-input-event event)
|
||||||
|
(command (key-binding [mouse-1] nil nil position)))
|
||||||
|
(should (commandp command))
|
||||||
|
(should (eq command (lookup-key map [mouse-1])))
|
||||||
|
(with-temp-buffer
|
||||||
|
(call-interactively command nil (vector event))
|
||||||
|
(should (zerop (buffer-size))))))
|
||||||
|
(ebox-playground-layer-test--parity)))
|
||||||
|
|
||||||
|
(defun ebox-playground-layer-test--nodes (form)
|
||||||
|
"Return DSL nodes in FORM without walking opaque property values."
|
||||||
|
(when (and (consp form) (memq (car form) '(box text row column flex grid)))
|
||||||
|
(let ((tail (cdr form)) (nodes (list form)))
|
||||||
|
(while tail
|
||||||
|
(if (keywordp (car tail))
|
||||||
|
(setq tail (cddr tail))
|
||||||
|
(setq nodes (append nodes (ebox-playground-layer-test--nodes (car tail)))
|
||||||
|
tail (cdr tail))))
|
||||||
|
nodes)))
|
||||||
|
|
||||||
|
(defun ebox-playground-layer-test--toolbar-hover (labels)
|
||||||
|
"Assert that LABELS own separate hover groups with inert toolbar gaps."
|
||||||
|
(let (groups bindings rows)
|
||||||
|
(dolist (label labels)
|
||||||
|
(let* ((position (ebox-playground-layer-test--position label))
|
||||||
|
(hover (get-text-property position 'mouse-face))
|
||||||
|
(map (get-text-property position 'keymap)))
|
||||||
|
(should hover)
|
||||||
|
(should (keymapp map))
|
||||||
|
(should (eq (get-text-property position 'pointer) 'hand))
|
||||||
|
;; Equal colors still need separate native mouse-face identities.
|
||||||
|
(should-not (memq hover groups))
|
||||||
|
(push hover groups)
|
||||||
|
(push (cons map hover) bindings)
|
||||||
|
(save-excursion
|
||||||
|
(goto-char position)
|
||||||
|
(cl-pushnew (cons (line-beginning-position) (line-end-position))
|
||||||
|
rows :test #'equal))))
|
||||||
|
(dolist (row rows)
|
||||||
|
(cl-loop for position from (car row) below (cdr row)
|
||||||
|
for map = (get-text-property position 'keymap)
|
||||||
|
do (if map
|
||||||
|
(progn
|
||||||
|
(should (assq map bindings))
|
||||||
|
(should (eq (get-text-property position 'mouse-face)
|
||||||
|
(cdr (assq map bindings))))
|
||||||
|
(should (eq (get-text-property position 'pointer) 'hand)))
|
||||||
|
;; This includes gaps between buttons and the row's tail.
|
||||||
|
(dolist (property '(mouse-face pointer keymap))
|
||||||
|
(should-not (get-text-property position property))))))))
|
||||||
|
|
||||||
|
(defun ebox-playground-layer-test--lower-note-padding-hover (label)
|
||||||
|
"Assert that LABEL's intentional block hover includes its blank padding."
|
||||||
|
(let* ((position (ebox-playground-layer-test--position label))
|
||||||
|
(hover (get-text-property position 'mouse-face))
|
||||||
|
(map (get-text-property position 'keymap)))
|
||||||
|
(should hover)
|
||||||
|
(should (keymapp map))
|
||||||
|
(save-excursion
|
||||||
|
(goto-char position)
|
||||||
|
(let ((padding (cl-loop for at from (line-beginning-position)
|
||||||
|
below (line-end-position)
|
||||||
|
when (and (get-text-property at 'ebox-pl)
|
||||||
|
(eq map (get-text-property at 'keymap)))
|
||||||
|
return at)))
|
||||||
|
(should padding)
|
||||||
|
(should (string-match-p
|
||||||
|
"\\`[[:space:]]+\\'"
|
||||||
|
(buffer-substring-no-properties padding (1+ padding))))
|
||||||
|
(should (eq hover (get-text-property padding 'mouse-face)))
|
||||||
|
(should (eq map (get-text-property padding 'keymap)))
|
||||||
|
(should (eq (get-text-property padding 'pointer) 'hand))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-layer-hover-matches-action-and-block-scopes ()
|
||||||
|
"Toolbar buttons hover separately while the note keeps its full surface."
|
||||||
|
(dolist (width '(60 720))
|
||||||
|
(ebox-playground-layer-test--with-preview (buffer width)
|
||||||
|
(let ((toolbars '(("EXPLODE STACK" "CYCLE FOCUS" "SWITCH MOOD")
|
||||||
|
("TOGGLE PANEL A" "MOVE PANEL A" "RAISE / LOWER A"
|
||||||
|
"UPDATE COVERED NOTE")
|
||||||
|
("CYCLE PLACEMENT" "MOVE ANCHOR"))))
|
||||||
|
(dolist (labels toolbars)
|
||||||
|
(ebox-playground-layer-test--toolbar-hover labels))
|
||||||
|
(ebox-playground-layer-test--activate "EXPLODE STACK")
|
||||||
|
(dolist (labels toolbars)
|
||||||
|
(ebox-playground-layer-test--toolbar-hover labels)))
|
||||||
|
(ebox-playground-layer-test--activate "TOGGLE PANEL A")
|
||||||
|
(ebox-playground-layer-test--lower-note-padding-hover "LOWER REV 00")
|
||||||
|
(ebox-playground-layer-test--activate "UPDATE COVERED NOTE")
|
||||||
|
(ebox-playground-layer-test--lower-note-padding-hover "LOWER REV 01"))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-layer-reference-declares-layers-with-one-font-size ()
|
||||||
|
"Actual layout data contains the taught contracts and no font-size override."
|
||||||
|
(let* ((form (ebox-playground--evaluate-form
|
||||||
|
(ebox-playground--read-file ebox-playground-layer-test--file)
|
||||||
|
ebox-playground-layer-test--file))
|
||||||
|
(nodes (ebox-playground-layer-test--nodes form))
|
||||||
|
(owner (cl-find-if
|
||||||
|
(lambda (node) (equal (plist-get (cdr node) :id) "menu-owner"))
|
||||||
|
nodes))
|
||||||
|
(menu (cl-find-if
|
||||||
|
(lambda (node) (equal (plist-get (cdr node) :id) "root-menu"))
|
||||||
|
(ebox-playground-layer-test--nodes owner))))
|
||||||
|
(should (ebox-canonical-input-p (ebox-build form)))
|
||||||
|
(should (eq (plist-get (cdr owner) :overflow) 'hidden))
|
||||||
|
(should (equal (plist-get (cdr owner) :height) '(lh 1)))
|
||||||
|
(should (eq (plist-get (cdr menu) :layer) 'root))
|
||||||
|
(should (equal (plist-get (cdr menu) :anchor) "menu-trigger"))
|
||||||
|
(dolist (position '(relative absolute))
|
||||||
|
(should (cl-some (lambda (node)
|
||||||
|
(eq (plist-get (cdr node) :position) position)) nodes)))
|
||||||
|
(dolist (node nodes)
|
||||||
|
(should-not (plist-member (cdr node) :isolation))
|
||||||
|
(should-not (plist-member (cdr node) :font-size)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-layer-covered-updates-reveal-latest-native-surface ()
|
||||||
|
"Covered text remains retained; revealing it restores current native help."
|
||||||
|
(ebox-playground-layer-test--with-preview (buffer)
|
||||||
|
(should-not (string-match-p "LOWER REV" (buffer-string)))
|
||||||
|
(should (ebox-region-resolve buffer "lower-note"))
|
||||||
|
(ebox-playground-layer-test--activate "UPDATE COVERED NOTE")
|
||||||
|
(ebox-playground-layer-test--activate "UPDATE COVERED NOTE" (kbd "SPC"))
|
||||||
|
(should-not (string-match-p "LOWER REV" (buffer-string)))
|
||||||
|
(should (string-match-p "lower rev=02" (buffer-string)))
|
||||||
|
(ebox-playground-layer-test--activate "TOGGLE PANEL A")
|
||||||
|
(let* ((position (ebox-playground-layer-test--position "LOWER REV 02"))
|
||||||
|
(help (get-text-property position 'help-echo)))
|
||||||
|
(should (eq (get-text-property position 'pointer) 'hand))
|
||||||
|
(should (get-text-property position 'mouse-face))
|
||||||
|
(should (functionp help))
|
||||||
|
(should (string-match-p "revision 2; activated 0"
|
||||||
|
(funcall help (selected-window) buffer position))))
|
||||||
|
(ebox-playground-layer-test--activate "LOWER REV 02" [return])
|
||||||
|
(let* ((position (ebox-playground-layer-test--position "LOWER REV 02"))
|
||||||
|
(help (get-text-property position 'help-echo)))
|
||||||
|
(should (string-match-p "revision 2; activated 1"
|
||||||
|
(funcall help (selected-window) buffer position))))
|
||||||
|
(ebox-playground-layer-test--activate "TOGGLE PANEL A")
|
||||||
|
(should-not (string-match-p "LOWER REV" (buffer-string)))
|
||||||
|
(ebox-playground-layer-test--activate "TOGGLE PANEL A")
|
||||||
|
(should (integerp (ebox-playground-layer-test--position "LOWER REV 02")))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-layer-move-and-raise-preserve-flow-and-coverage ()
|
||||||
|
"Moving changes paint coordinates; raising changes only overlap ownership."
|
||||||
|
(ebox-playground-layer-test--with-preview (buffer)
|
||||||
|
(let ((home (ebox-playground-layer-test--coordinates "PANEL A / blue"))
|
||||||
|
(desk-end (ebox-playground-layer-test--coordinates "DESK END")))
|
||||||
|
(should-not (string-match-p "A survives under the amber panel" (buffer-string)))
|
||||||
|
(ebox-playground-layer-test--activate "RAISE / LOWER A")
|
||||||
|
(should (integerp (ebox-playground-layer-test--position
|
||||||
|
"A survives under the amber panel")))
|
||||||
|
(should-not (string-match-p "PANEL B / amber" (buffer-string)))
|
||||||
|
(should (equal home (ebox-playground-layer-test--coordinates "PANEL A / blue")))
|
||||||
|
(ebox-playground-layer-test--activate "RAISE / LOWER A")
|
||||||
|
(should (integerp (ebox-playground-layer-test--position "PANEL B / amber")))
|
||||||
|
(ebox-playground-layer-test--activate "MOVE PANEL A")
|
||||||
|
(let ((moved (ebox-playground-layer-test--coordinates "PANEL A / blue")))
|
||||||
|
(should (> (car moved) (car home)))
|
||||||
|
(should (= (cdr moved) (- (cdr home) 2))))
|
||||||
|
(should (equal desk-end (ebox-playground-layer-test--coordinates "DESK END")))
|
||||||
|
(ebox-playground-layer-test--activate "MOVE PANEL A")
|
||||||
|
(should (equal home (ebox-playground-layer-test--coordinates "PANEL A / blue"))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-layer-relative-marker-reserves-its-original-row ()
|
||||||
|
"The relative marker moves into row two while row three retains its slot."
|
||||||
|
(ebox-playground-layer-test--with-preview (buffer)
|
||||||
|
(let ((marker (ebox-playground-layer-test--coordinates "RELATIVE / +4ch, +1lh"))
|
||||||
|
(following (ebox-playground-layer-test--coordinates "FLOW ROW 3")))
|
||||||
|
(should (= (1+ (cdr marker)) (cdr following)))
|
||||||
|
(should (> (car marker) (car following)))
|
||||||
|
(save-excursion
|
||||||
|
(goto-char (ebox-playground-layer-test--position "RELATIVE / +4ch, +1lh"))
|
||||||
|
(forward-line -1)
|
||||||
|
(should (string-match-p
|
||||||
|
"\\`[[:space:]]*\\'"
|
||||||
|
(buffer-substring-no-properties
|
||||||
|
(line-beginning-position) (line-end-position)))))
|
||||||
|
(ebox-playground-layer-test--parity))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-layer-root-menu-follows-all-four-anchor-placements ()
|
||||||
|
"The nested menu escapes its one-row owner and follows the moved trigger."
|
||||||
|
(ebox-playground-layer-test--with-preview (buffer)
|
||||||
|
(should-not (string-match-p "ROOT MENU / choose" (buffer-string)))
|
||||||
|
(ebox-playground-layer-test--activate "OPEN / CLOSE ROOT MENU")
|
||||||
|
(let* ((trigger (ebox-playground-layer-test--coordinates "OPEN / CLOSE ROOT MENU"))
|
||||||
|
(bottom-start (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||||
|
(should (= (cdr bottom-start) (1+ (cdr trigger))))
|
||||||
|
(should (= (car bottom-start) (car trigger)))
|
||||||
|
(ebox-playground-layer-test--activate "CYCLE PLACEMENT")
|
||||||
|
(let ((bottom-end (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||||
|
(should (= (cdr bottom-end) (cdr bottom-start)))
|
||||||
|
(should (> (car bottom-end) (car bottom-start)))
|
||||||
|
(ebox-playground-layer-test--activate "CYCLE PLACEMENT")
|
||||||
|
(let ((top-start (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||||
|
(should (= (cdr top-start) (- (cdr trigger) 3)))
|
||||||
|
(should (= (car top-start) (car bottom-start))))
|
||||||
|
(ebox-playground-layer-test--activate "CYCLE PLACEMENT")
|
||||||
|
(let ((top-end (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||||
|
(should (= (cdr top-end) (- (cdr trigger) 3)))
|
||||||
|
(should (= (car top-end) (car bottom-end))))
|
||||||
|
(ebox-playground-layer-test--activate "CYCLE PLACEMENT"))
|
||||||
|
(should (equal bottom-start (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||||
|
(ebox-playground-layer-test--activate "MOVE ANCHOR")
|
||||||
|
(let ((moved-trigger (ebox-playground-layer-test--coordinates "OPEN / CLOSE ROOT MENU"))
|
||||||
|
(moved-menu (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
|
||||||
|
(should (> (car moved-trigger) (car trigger)))
|
||||||
|
(should (= (- (car moved-menu) (car bottom-start))
|
||||||
|
(- (car moved-trigger) (car trigger))))))
|
||||||
|
(let ((position (ebox-playground-layer-test--position "CHOOSE SAGE")))
|
||||||
|
(dolist (property '(keymap mouse-face help-echo pointer))
|
||||||
|
(should (get-text-property position property))))
|
||||||
|
(ebox-playground-layer-test--click "CHOOSE SAGE")
|
||||||
|
(should-not (string-match-p "ROOT MENU / choose" (buffer-string)))
|
||||||
|
(should (integerp (ebox-playground-layer-test--position
|
||||||
|
"Selected sage from the anchored menu.")))
|
||||||
|
(ebox-playground-layer-test--activate "OPEN / CLOSE ROOT MENU")
|
||||||
|
(ebox-playground-layer-test--activate "CHOOSE AMBER" (kbd "SPC"))
|
||||||
|
(should (integerp (ebox-playground-layer-test--position
|
||||||
|
"Selected amber from the anchored menu.")))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-layer-previews-retain-independent-state-at-two-widths ()
|
||||||
|
"Separate previews keep independent notes and menus, with bounded rendering."
|
||||||
|
(ebox-playground-layer-test--with-preview (first)
|
||||||
|
(ebox-playground-layer-test--activate "UPDATE COVERED NOTE")
|
||||||
|
(ebox-playground-layer-test--with-preview (second 1000)
|
||||||
|
(should (string-match-p "lower rev=00" (buffer-string)))
|
||||||
|
(ebox-playground-layer-test--activate "OPEN / CLOSE ROOT MENU")
|
||||||
|
(with-current-buffer first
|
||||||
|
(should (string-match-p "lower rev=01" (buffer-string)))
|
||||||
|
(should-not (string-match-p "ROOT MENU / choose" (buffer-string)))
|
||||||
|
(ebox-playground-layer-test--parity))
|
||||||
|
(dolist (line (ebox-string-lines (buffer-string)))
|
||||||
|
(should (<= (ebox-string-pixel-width line) 1000)))
|
||||||
|
(ebox-playground-layer-test--parity))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-layer-orbit-expands-without-moving-following-content ()
|
||||||
|
"The spatial desktop moves its cards while keeping the next section fixed."
|
||||||
|
(ebox-playground-layer-test--with-preview (buffer)
|
||||||
|
(let ((home (ebox-playground-layer-test--coordinates "SIGNAL / NOW PLAYING"))
|
||||||
|
(following (ebox-playground-layer-test--coordinates "01 / LAYERED DESK")))
|
||||||
|
(ebox-playground-layer-test--activate "EXPLODE STACK")
|
||||||
|
(let ((expanded (ebox-playground-layer-test--coordinates "SIGNAL / NOW PLAYING")))
|
||||||
|
(should (> (car expanded) (car home)))
|
||||||
|
(should (= (cdr expanded) (+ (cdr home) 4))))
|
||||||
|
(should (equal following
|
||||||
|
(ebox-playground-layer-test--coordinates "01 / LAYERED DESK")))
|
||||||
|
(should (string-match-p "EXPANDED" (buffer-string)))
|
||||||
|
(ebox-playground-layer-test--activate "EXPLODE STACK" (kbd "SPC"))
|
||||||
|
(should (equal home
|
||||||
|
(ebox-playground-layer-test--coordinates "SIGNAL / NOW PLAYING")))
|
||||||
|
(should (string-match-p "STACKED" (buffer-string))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-layer-orbit-focus-and-palette-are-reversible ()
|
||||||
|
"Native commands update actual card order and paint, independently per preview."
|
||||||
|
(ebox-playground-layer-test--with-preview (first)
|
||||||
|
(cl-labels ((property (id key)
|
||||||
|
(let* ((handle (ebox-region-resolve (current-buffer) id))
|
||||||
|
(region-id (cdr (ebox-selector--region-target handle))))
|
||||||
|
(ebox-get (ebox--buffer-region-render-owner-node
|
||||||
|
(current-buffer) region-id) key))))
|
||||||
|
(dolist (card '(("orbit-violet" . "STUDIO")
|
||||||
|
("orbit-lime" . "NEXT")
|
||||||
|
("orbit-signal" . "SIGNAL")))
|
||||||
|
(ebox-playground-layer-test--activate "CYCLE FOCUS")
|
||||||
|
(should (= (property (car card) :z-index) 6))
|
||||||
|
(should (string-match-p (concat (cdr card) " IN FRONT") (buffer-string))))
|
||||||
|
(ebox-playground-layer-test--click "SWITCH MOOD")
|
||||||
|
(should (equal (property "orbit-signal" :bgcolor) "#FFD49B"))
|
||||||
|
(should (string-match-p "SOLAR PALETTE" (buffer-string)))
|
||||||
|
(ebox-playground-layer-test--with-preview (second 1000)
|
||||||
|
(should (string-match-p "LUNAR PALETTE" (buffer-string)))
|
||||||
|
(should (equal (property "orbit-signal" :bgcolor) "#B9E9F2")))
|
||||||
|
(ebox-playground-layer-test--activate "SWITCH MOOD")
|
||||||
|
(should (equal (property "orbit-signal" :bgcolor) "#B9E9F2"))
|
||||||
|
(should (string-match-p "LUNAR PALETTE" (buffer-string))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-layer-orbit-remains-bounded-at-desktop-widths ()
|
||||||
|
"The actual composition and toolbar remain usable across desktop widths."
|
||||||
|
(dolist (width '(80 104 140))
|
||||||
|
(ebox-playground-layer-test--with-preview (buffer width)
|
||||||
|
(should (integerp (ebox-playground-layer-test--position "STUDIO / 01")))
|
||||||
|
(should (integerp (ebox-playground-layer-test--position "SIGNAL / NOW PLAYING")))
|
||||||
|
(ebox-playground-layer-test--activate "EXPLODE STACK")
|
||||||
|
(dolist (line (ebox-string-lines (buffer-string)))
|
||||||
|
(should (<= (ebox-string-pixel-width line) width))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-layer-orbit-commands-publish-local-owners-once ()
|
||||||
|
"Orbit commands publish once without preparing the unrelated full surface."
|
||||||
|
(ebox-playground-layer-test--with-preview (buffer 104)
|
||||||
|
(dolist (label '("EXPLODE STACK" "EXPLODE STACK"
|
||||||
|
"CYCLE FOCUS" "CYCLE FOCUS" "CYCLE FOCUS"
|
||||||
|
"CYCLE FOCUS" "CYCLE FOCUS" "CYCLE FOCUS"
|
||||||
|
"SWITCH MOOD" "EXPLODE STACK" "CYCLE FOCUS"
|
||||||
|
"EXPLODE STACK" "SWITCH MOOD"))
|
||||||
|
(let ((revision (plist-get (ebox-surface-buffer-snapshot buffer) :revision)))
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (&rest _)
|
||||||
|
(ert-fail "Orbit command prepared the full surface")))
|
||||||
|
((symbol-function 'ebox-playground-layer-test--parity)
|
||||||
|
#'ignore))
|
||||||
|
(ebox-playground-layer-test--activate label))
|
||||||
|
(ebox-playground-layer-test--parity)
|
||||||
|
(should (= (plist-get (ebox-surface-buffer-snapshot buffer) :revision)
|
||||||
|
(1+ revision)))))))
|
||||||
|
|
||||||
|
(provide 'ebox-playground-layer-tests)
|
||||||
|
;;; ebox-playground-layer-tests.el ends here
|
||||||
377
tests/ebox-playground-size-tests.el
Normal file
377
tests/ebox-playground-size-tests.el
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
;;; ebox-playground-size-tests.el --- Size reference contracts -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Keep the Size reference executable and verify declarations, arithmetic,
|
||||||
|
;; and viewport updates instead of treating descriptive labels as coverage.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ebox-playground)
|
||||||
|
(require 'ebox-size)
|
||||||
|
(require 'ebox-playground-flex-resize-evaluator
|
||||||
|
(expand-file-name "scripts/ebox-playground-flex-resize-evaluator.el"
|
||||||
|
ebox-playground-directory))
|
||||||
|
|
||||||
|
(defconst ebox-playground-size-test--file
|
||||||
|
(expand-file-name "examples/size-reference.ebox" ebox-playground-directory)
|
||||||
|
"Size reference checked by this suite.")
|
||||||
|
|
||||||
|
(defun ebox-playground-size-test--form ()
|
||||||
|
"Read the actual Size example through the ordinary file boundary."
|
||||||
|
(ebox-playground--evaluate-form
|
||||||
|
(ebox-playground--read-file ebox-playground-size-test--file)))
|
||||||
|
|
||||||
|
(defun ebox-playground-size-test--lists (form)
|
||||||
|
"Return the nonempty lists recursively contained in FORM."
|
||||||
|
(when (consp form)
|
||||||
|
(cons form (cl-mapcan #'ebox-playground-size-test--lists form))))
|
||||||
|
|
||||||
|
(defun ebox-playground-size-test--nodes (form)
|
||||||
|
"Return actual DSL nodes in FORM, excluding size expressions."
|
||||||
|
(cl-remove-if-not
|
||||||
|
(lambda (item) (memq (car item) '(box text row column flex grid)))
|
||||||
|
(ebox-playground-size-test--lists form)))
|
||||||
|
|
||||||
|
(defun ebox-playground-size-test--node (id)
|
||||||
|
"Return the actual demonstration node named ID."
|
||||||
|
(or (cl-find-if
|
||||||
|
(lambda (node) (equal (format "%s" (plist-get (cdr node) :id)) id))
|
||||||
|
(ebox-playground-size-test--nodes (ebox-playground-size-test--form)))
|
||||||
|
(ert-fail (format "Missing Size demonstration: %s" id))))
|
||||||
|
|
||||||
|
(defun ebox-playground-size-test--card (id)
|
||||||
|
"Return the authored outer card containing demonstration ID."
|
||||||
|
(or (cl-find-if
|
||||||
|
(lambda (node)
|
||||||
|
(and (eq (car node) 'column)
|
||||||
|
(plist-get (cdr node) :border)
|
||||||
|
(cl-some (lambda (child)
|
||||||
|
(equal (plist-get (cdr child) :id) id))
|
||||||
|
(ebox-playground-size-test--nodes node))))
|
||||||
|
(ebox-playground-size-test--nodes (ebox-playground-size-test--form)))
|
||||||
|
(ert-fail (format "Missing Size card: %s" id))))
|
||||||
|
|
||||||
|
(defun ebox-playground-size-test--color-widths (line color)
|
||||||
|
"Return contiguous visible background COLOR widths in rendered LINE."
|
||||||
|
(let ((line (ebox-playground-flex-resize-evaluator--visual-text line))
|
||||||
|
(start 0) (run 0) widths)
|
||||||
|
(while (< start (length line))
|
||||||
|
(let ((end (next-single-property-change
|
||||||
|
start 'face line (length line))))
|
||||||
|
(if (equal (plist-get (get-text-property start 'face line) :background)
|
||||||
|
color)
|
||||||
|
(setq run (+ run (ebox-string-pixel-width
|
||||||
|
(substring line start end))))
|
||||||
|
(when (> run 0) (push run widths))
|
||||||
|
(setq run 0))
|
||||||
|
(setq start end)))
|
||||||
|
(when (> run 0) (push run widths))
|
||||||
|
(nreverse widths)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-covers-values-in-real-declarations ()
|
||||||
|
"Every advertised unit, function, and keyword must occur as executable data."
|
||||||
|
(let* ((form (ebox-playground-size-test--form))
|
||||||
|
(nodes (ebox-playground-size-test--nodes form))
|
||||||
|
(lists (ebox-playground-size-test--lists form)))
|
||||||
|
(dolist (unit '(px % vw vh ch lh))
|
||||||
|
(should (cl-some (lambda (item)
|
||||||
|
(and (eq (car item) unit)
|
||||||
|
(= (length item) 2)
|
||||||
|
(numberp (cadr item))))
|
||||||
|
lists)))
|
||||||
|
(dolist (function '(calc min max clamp + - * /))
|
||||||
|
(should (cl-some (lambda (item) (eq (car item) function)) lists)))
|
||||||
|
(should (cl-some (lambda (item)
|
||||||
|
(and (memq (car item) '(px % vw vh ch lh))
|
||||||
|
(floatp (cadr item))))
|
||||||
|
lists))
|
||||||
|
(dolist (property '(:width :height :min-width :min-height
|
||||||
|
:max-width :max-height))
|
||||||
|
(dolist (value (append
|
||||||
|
(if (memq property '(:max-width :max-height))
|
||||||
|
'(none) '(auto))
|
||||||
|
'(min-content max-content fit-content stretch)))
|
||||||
|
(ert-info ((format "Missing actual %S %S demonstration" property value))
|
||||||
|
(should (cl-some (lambda (node)
|
||||||
|
(and (plist-member (cdr node) property)
|
||||||
|
(eq (plist-get (cdr node) property) value)))
|
||||||
|
nodes)))))
|
||||||
|
(should (ebox-canonical-input-p (ebox-build form)))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-renders-full-document-at-three-widths ()
|
||||||
|
"The complete reference remains readable beyond its initially visible page."
|
||||||
|
(dolist (width '(320 720 1000))
|
||||||
|
(let* ((text (ebox-playground-flex-resize-evaluator--example-text
|
||||||
|
ebox-playground-size-test--file width t))
|
||||||
|
(lines (ebox-string-lines text)))
|
||||||
|
(should (string-match-p "Ebox Size Reference" text))
|
||||||
|
(should (> (length lines) 30))
|
||||||
|
(dolist (line lines)
|
||||||
|
(should (<= (ebox-string-pixel-width line) width))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-expressions-have-known-results ()
|
||||||
|
"Check the example's actual expressions against independent reference values."
|
||||||
|
(let ((context '(:percent-base 220 :viewport-width 800 :viewport-height 600
|
||||||
|
:ch 8 :lh 20)))
|
||||||
|
(dolist (case '(("size-px" :width 180)
|
||||||
|
("size-percent" :width 110)
|
||||||
|
("size-vw" :width 160)
|
||||||
|
("size-ch" :width 96)
|
||||||
|
("size-lh" :height 40)
|
||||||
|
("size-vh" :height 60)
|
||||||
|
("size-calc" :width 196)
|
||||||
|
("size-min" :width 100)
|
||||||
|
("size-max" :width 110)
|
||||||
|
("size-arithmetic" :width 60)
|
||||||
|
("size-nested" :width 160)))
|
||||||
|
(let* ((node (ebox-playground-size-test--node (car case)))
|
||||||
|
(expression (plist-get (cdr node) (cadr case))))
|
||||||
|
(ert-info ((format "Size demo %s: %S" (car case) expression))
|
||||||
|
(should (= (ebox-size-resolve expression context) (nth 2 case))))))
|
||||||
|
(should (= (ebox-size-resolve
|
||||||
|
(plist-get (cdr (ebox-playground-size-test--node
|
||||||
|
"size-percent-parent")) :width)
|
||||||
|
context)
|
||||||
|
222))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-labels-keep-a-common-font ()
|
||||||
|
"Only the title is bold and only measured font samples change text size."
|
||||||
|
(let ((nodes (ebox-playground-size-test--nodes
|
||||||
|
(ebox-playground-size-test--form)))
|
||||||
|
font-samples bold-nodes)
|
||||||
|
(dolist (node nodes)
|
||||||
|
(when (plist-member (cdr node) :font-size)
|
||||||
|
(push (plist-get (cdr node) :id) font-samples))
|
||||||
|
(when (eq (plist-get (cdr node) :font-weight) 'bold)
|
||||||
|
(push node bold-nodes))
|
||||||
|
(should-not (plist-member (cdr node) :padding-block)))
|
||||||
|
(should (equal (sort font-samples #'string-lessp)
|
||||||
|
'("size-font-large" "size-font-small")))
|
||||||
|
(should (= (length bold-nodes) 1))
|
||||||
|
(should (equal (car (last (car bold-nodes))) "Ebox Size Reference"))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-card-and-target-widths-match-copy ()
|
||||||
|
"Actual borders and colored targets use the stated 220/112 px references."
|
||||||
|
(dolist (case '(("size-percent" "#D97757" 222 110)
|
||||||
|
("size-calc" "#AFA3D1" 222 196)
|
||||||
|
("size-max" "#AFA3D1" 222 110)
|
||||||
|
("size-width-auto" "#7DB8CE" 114 112)
|
||||||
|
("size-width-stretch" "#7DB8CE" 114 112)
|
||||||
|
("size-max-width-none" "#7DB8CE" 114 112)))
|
||||||
|
(let* ((text (ebox-render
|
||||||
|
(ebox-build (ebox-playground-size-test--card (car case)))))
|
||||||
|
(lines (ebox-string-lines text))
|
||||||
|
(target-widths
|
||||||
|
(cl-mapcan (lambda (line)
|
||||||
|
(ebox-playground-size-test--color-widths
|
||||||
|
line (nth 1 case)))
|
||||||
|
lines)))
|
||||||
|
(ert-info ((format "Card %s" (car case)))
|
||||||
|
(should (cl-every (lambda (line)
|
||||||
|
(= (ebox-string-pixel-width line) (nth 2 case)))
|
||||||
|
lines))
|
||||||
|
(should (equal target-widths (make-list 2 (nth 3 case))))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-clipping-and-box-model-match-copy ()
|
||||||
|
"Descriptions must agree with real clipping and visible box-model content."
|
||||||
|
(dolist (id '("size-fractional-height" "size-whole-height"))
|
||||||
|
(let ((text (ebox-render
|
||||||
|
(ebox-build (ebox-playground-size-test--node id)))))
|
||||||
|
(should (= (ebox-string-height text) 2))
|
||||||
|
(should (string-match-p "line one" text))
|
||||||
|
(should (string-match-p "line two" text))
|
||||||
|
(should-not (string-match-p "line three" text))))
|
||||||
|
(let ((text (ebox-render
|
||||||
|
(ebox-build (ebox-playground-size-test--node "size-box-model")))))
|
||||||
|
(should (string-match-p "padding" text))
|
||||||
|
(should (string-match-p "inside the border" text)))
|
||||||
|
(let ((width (plist-get
|
||||||
|
(cdr (ebox-playground-size-test--node "size-fractional-width"))
|
||||||
|
:width)))
|
||||||
|
(should (= (ebox-size-resolve width '(:viewport-width 853)) 102.36))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-height-equivalence-is-intentional ()
|
||||||
|
"Content height keywords agree, while lower bounds and stretch differ."
|
||||||
|
(dolist (property '("height" "min-height" "max-height"))
|
||||||
|
(dolist (keyword (cons (if (equal property "max-height") "none" "auto")
|
||||||
|
'("min-content" "max-content" "fit-content" "stretch")))
|
||||||
|
(let* ((id (format "size-%s-%s" property keyword))
|
||||||
|
(text (ebox-render
|
||||||
|
(ebox-build (ebox-playground-size-test--card id))))
|
||||||
|
(colored-lines
|
||||||
|
(cl-count-if
|
||||||
|
(lambda (line)
|
||||||
|
(ebox-playground-size-test--color-widths line "#D9C46D"))
|
||||||
|
(ebox-string-lines text)))
|
||||||
|
(expected (cond ((member keyword '("stretch" "none")) 5)
|
||||||
|
((and (equal property "min-height")
|
||||||
|
(equal keyword "auto")) 1)
|
||||||
|
(t 3))))
|
||||||
|
(ert-info ((format "%s visible height" id))
|
||||||
|
(should (= colored-lines expected)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-long-content-shows-three-widths ()
|
||||||
|
"The same constrained text visibly separates all three intrinsic widths."
|
||||||
|
(let ((widths
|
||||||
|
(mapcar
|
||||||
|
(lambda (keyword)
|
||||||
|
(let* ((id (concat "size-width-long-" keyword))
|
||||||
|
(text (ebox-render
|
||||||
|
(ebox-build (ebox-playground-size-test--card id))))
|
||||||
|
(runs (cl-mapcan
|
||||||
|
(lambda (line)
|
||||||
|
(ebox-playground-size-test--color-widths
|
||||||
|
line "#7DB8CE"))
|
||||||
|
(ebox-string-lines text))))
|
||||||
|
;; Width, minimum, and maximum samples share one card.
|
||||||
|
(should (= (length runs) 12))
|
||||||
|
(should (cl-every (lambda (width) (= width (car runs))) runs))
|
||||||
|
;; All words survive all three constraints without being split
|
||||||
|
;; using the obsolete preferred width or clipped from the box.
|
||||||
|
(dolist (word '("red" "ink" "blue"))
|
||||||
|
(should (= (length (split-string text word)) 4)))
|
||||||
|
(car runs)))
|
||||||
|
'("min-content" "fit-content" "max-content"))))
|
||||||
|
(should (< (nth 0 widths) (nth 1 widths) (nth 2 widths)))
|
||||||
|
(should (= (nth 0 widths) (ebox-string-pixel-width "blue")))
|
||||||
|
(should (= (nth 1 widths) (* 8 (ebox-string-pixel-width "0"))))
|
||||||
|
(should (= (nth 2 widths) (ebox-string-pixel-width "red ink on blue")))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-row-distinguishes-auto-and-stretch ()
|
||||||
|
"Row context makes automatic content sizing visibly shorter than stretch."
|
||||||
|
(let ((widths
|
||||||
|
(mapcar
|
||||||
|
(lambda (keyword)
|
||||||
|
(let* ((text (ebox-render
|
||||||
|
(ebox-build
|
||||||
|
(ebox-playground-size-test--card
|
||||||
|
(concat "size-width-row-" keyword)))))
|
||||||
|
(runs (cl-mapcan
|
||||||
|
(lambda (line)
|
||||||
|
(ebox-playground-size-test--color-widths
|
||||||
|
line "#7DB8CE"))
|
||||||
|
(ebox-string-lines text))))
|
||||||
|
(should runs)
|
||||||
|
(should (cl-every (lambda (width) (= width (car runs))) runs))
|
||||||
|
(car runs)))
|
||||||
|
'("auto" "stretch"))))
|
||||||
|
(should (< (car widths) (cadr widths)))
|
||||||
|
(should (= (car widths) (ebox-string-pixel-width "red ink")))
|
||||||
|
(should (= (cadr widths) 112))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-functions-show-boundary-transitions ()
|
||||||
|
"Actual rendered bars distinguish capping, flooring, and both constraints."
|
||||||
|
(dolist (case '(("min" 24 96 144) ("max" 48 96 192)
|
||||||
|
("clamp" 48 96 144)))
|
||||||
|
(cl-mapc
|
||||||
|
(lambda (regime expected)
|
||||||
|
(let* ((id (format "size-function-%s-%s" (car case) regime))
|
||||||
|
(text (ebox-render
|
||||||
|
(ebox-build (ebox-playground-size-test--node id)))))
|
||||||
|
(ert-info ((format "%s rendered extent" id))
|
||||||
|
(should (= (apply #'max (mapcar #'ebox-string-pixel-width
|
||||||
|
(ebox-string-lines text)))
|
||||||
|
expected)))))
|
||||||
|
'("low" "mid" "high") (cdr case))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-units-reach-painted-sizes ()
|
||||||
|
"Verify units and percentages in actual parent and viewport contexts."
|
||||||
|
(let ((ebox-viewport-width 800) (ebox-viewport-height 40))
|
||||||
|
(dolist (case '(("size-percent" "#D97757" 110 2)
|
||||||
|
("size-vw" "#D97757" 160 2)
|
||||||
|
("size-ch" "#D97757" 12 2)
|
||||||
|
("size-lh" "#D97757" 220 2)
|
||||||
|
("size-vh" "#D97757" 220 4)
|
||||||
|
("size-percent-height" "#8FAF95" 220 3)))
|
||||||
|
(let* ((text (ebox-render
|
||||||
|
(ebox-build (ebox-playground-size-test--card (car case)))))
|
||||||
|
(runs (cl-mapcan
|
||||||
|
(lambda (line)
|
||||||
|
(ebox-playground-size-test--color-widths line (nth 1 case)))
|
||||||
|
(ebox-string-lines text)))
|
||||||
|
(width (if (equal (car case) "size-ch")
|
||||||
|
(min 220 (* 12 (ebox-string-pixel-width "0")))
|
||||||
|
(nth 2 case))))
|
||||||
|
(ert-info ((format "%s painted size" (car case)))
|
||||||
|
(should (equal runs (make-list (nth 3 case) width))))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-flex-growth-has-visible-shares ()
|
||||||
|
"Equal bases expose spare-space growth without font-dependent wrapping."
|
||||||
|
(let* ((ebox-viewport-width 220)
|
||||||
|
(text (ebox-render
|
||||||
|
(ebox-build (ebox-playground-size-test--node "size-flex-grow"))))
|
||||||
|
(lines (ebox-string-lines text)))
|
||||||
|
(should (= (length lines) 2))
|
||||||
|
(dolist (line lines)
|
||||||
|
(should (= (ebox-string-pixel-width line) 220))
|
||||||
|
(should (equal (ebox-playground-size-test--color-widths line "#9FD5CB")
|
||||||
|
'(80)))
|
||||||
|
(should (equal (ebox-playground-size-test--color-widths line "#71BDB0")
|
||||||
|
'(130))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-grid-paint-shows-complete-tracks ()
|
||||||
|
"Cell backgrounds expose both lines of each track and leave the gap clear."
|
||||||
|
(let* ((ebox-viewport-width 220)
|
||||||
|
(text (ebox-render
|
||||||
|
(ebox-build (ebox-playground-size-test--node "size-grid"))))
|
||||||
|
(lines (ebox-string-lines text))
|
||||||
|
(ch (ebox-string-pixel-width "0")))
|
||||||
|
(should (= (length lines) 5))
|
||||||
|
(cl-loop for line in lines for index from 0 do
|
||||||
|
(should (= (ebox-string-pixel-width line) 220))
|
||||||
|
(dolist (case `(("#9FD5CB" . ,(* 8 ch))
|
||||||
|
("#71BDB0" . ,(- 220 (* 9 ch)))))
|
||||||
|
(should (equal
|
||||||
|
(ebox-playground-size-test--color-widths line (car case))
|
||||||
|
(unless (= index 2) (list (cdr case)))))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-clamp-reaches-all-three-regimes ()
|
||||||
|
"The responsive clamp demo must exercise its minimum, fluid value, and cap."
|
||||||
|
(let ((expression (plist-get
|
||||||
|
(cdr (ebox-playground-size-test--node "size-clamp"))
|
||||||
|
:width)))
|
||||||
|
(dolist (case '((300 . 48) (800 . 96) (2000 . 200)))
|
||||||
|
(should (= (ebox-size-resolve expression
|
||||||
|
(list :viewport-width (car case)))
|
||||||
|
(cdr case))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-fixed-demos-render-their-extents ()
|
||||||
|
"Fixed pixel and line demonstrations must reach the public renderer intact."
|
||||||
|
(dolist (case '(("size-px" . 180) ("size-arithmetic" . 60)))
|
||||||
|
(let ((text (ebox-render
|
||||||
|
(ebox-build (ebox-playground-size-test--node (car case))))))
|
||||||
|
(should (= (apply #'max (mapcar #'ebox-string-pixel-width
|
||||||
|
(ebox-string-lines text)))
|
||||||
|
(cdr case)))))
|
||||||
|
(should (= (ebox-string-height
|
||||||
|
(ebox-render (ebox-build
|
||||||
|
(ebox-playground-size-test--node "size-lh"))))
|
||||||
|
2)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-playground-size-reference-retained-resize-matches-fresh-render ()
|
||||||
|
"Changing both viewport axes must update responsive sizes without stale output."
|
||||||
|
(let ((buffer (generate-new-buffer " *ebox-size-reference-test*"))
|
||||||
|
(ebox-runtime-idle-prewarm nil)
|
||||||
|
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((ebox-viewport-width 720) (ebox-viewport-height 36))
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
buffer (ebox-playground-view ebox-playground-size-test--file 720))
|
||||||
|
(dolist (size '((1000 48) (480 24) (720 36)))
|
||||||
|
(let* ((ebox-viewport-width (car size))
|
||||||
|
(ebox-viewport-height (cadr size))
|
||||||
|
(fresh (ebox-render
|
||||||
|
(ebox-playground-view
|
||||||
|
ebox-playground-size-test--file (car size)))))
|
||||||
|
(ebox-rerender-buffer-with-context buffer (car size) (cadr size))
|
||||||
|
(should
|
||||||
|
(equal-including-properties
|
||||||
|
(ebox-playground-flex-resize-evaluator--visual-text fresh)
|
||||||
|
(ebox-playground-flex-resize-evaluator--visual-text
|
||||||
|
(with-current-buffer buffer (buffer-string))))))))
|
||||||
|
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||||
|
|
||||||
|
(provide 'ebox-playground-size-tests)
|
||||||
|
;;; ebox-playground-size-tests.el ends here
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user