feat: add draggable desktop window example

This commit is contained in:
Kinneyzhang 2026-09-11 01:06:44 +08:00
parent ecc490d8c6
commit b94e19ad7a
9 changed files with 968 additions and 6 deletions

View File

@ -23,3 +23,12 @@ Keep necessary business functions and variables in a same-basename `.el` file be
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 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 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, and title-bar dragging,
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, 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.

View File

@ -2,7 +2,7 @@
## Source of truth ## Source of truth
- Status: Active - Status: Active
- Last refreshed: 2026-09-08 - 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
@ -82,3 +82,36 @@
## 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.
- 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.
- 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.
- 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.

View File

@ -2,10 +2,28 @@
## 真相来源 ## 真相来源
- 状态:有效 - 状态:有效
- 最近更新2026-09-08 - 最近更新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。各窗口有独立内容和局部层叠全部使用当前字体大小标题栏与底部窗口栏标出
活动窗口,不添加假的关闭按钮或自动动画。
点击露出的标题、正文或底部窗口入口时整窗置顶;按住标题栏拖拽时即时发布位置,保留
鼠标抓取偏移,并应用松手的最终位置。横向按像素,纵向遵循文本行网格。中断保留最后
成功的位置,手势退出不留下 timer、advice 或全局键位。RET/SPC 激活,标题或窗口栏的
Alt+方向键移动窗口RESET 恢复原始位置与次序。业务状态只在 Ebox 发布成功后提交,
每个预览独立。
结构为紧凑页头与工具栏、裁剪的主工作区、固定窗口栏和坐标状态;推荐至少 40 列、
20 行。较小尺寸保留窗口入口RESET 可找回拖出边界的窗口。测试覆盖真实 keymap
命令、中间运动帧、松手、遮挡归属、失败回滚及独立渲染一致性。遵守不操作用户 GUI
Emacs 的现有限制,批处理验证不等同于原生重绘延迟或截图验收。
## 品牌 ## 品牌
- 个性:精确、现代、克制、适合教学。 - 个性:精确、现代、克制、适合教学。
- 信任信号:像素对齐的分区、准确的 Grid 放置、明确的文字对比度、只使用公开 API以及不依赖主题 face 也清晰的布局。 - 信任信号:像素对齐的分区、准确的 Grid 放置、明确的文字对比度、只使用公开 API以及不依赖主题 face 也清晰的布局。
@ -13,7 +31,7 @@
## 产品目标 ## 产品目标
- 目标:用一个精致的独立示例演示 Ebox Grid 与组合能力,让底层包达到 Flex 参考示例的视觉质量标准。 - 目标:用一个精致的独立示例演示 Ebox Grid 与组合能力,让底层包达到 Flex 参考示例的视觉质量标准。
- 非目标ETAF Component、应用状态、浏览器仿制品,或第二套 Playground 框架 - 非目标ETAF Component、通用应用框架,或第二套 Playground 运行器。交互示例通过同名 companion 维护自己的小型状态
- 成功信号:第一屏能清楚表达层级和 Grid 行为;每个带色区块的文字都清晰可读;没有内容越过可见视口。 - 成功信号:第一屏能清楚表达层级和 Grid 行为;每个带色区块的文字都清晰可读;没有内容越过可见视口。
## 用户与任务 ## 用户与任务

View File

@ -7,7 +7,7 @@ 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 size-tests interaction-tests layer-tests 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
@ -26,8 +26,15 @@ test: compile
--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 -l tests/ebox-playground-size-tests.el \ -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-interaction-tests.el -l tests/ebox-playground-layer-tests.el \
-l tests/ebox-playground-desktop-tests.el \
-f ert-run-tests-batch-and-exit -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 layer-tests: compile
$(MAKE) -C ../ebox layer-tests EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)" $(MAKE) -C ../ebox layer-tests EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
$(EMACS) -Q --batch $(LOAD_PATH) \ $(EMACS) -Q --batch $(LOAD_PATH) \

View File

@ -1,6 +1,6 @@
# 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/`. `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. `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.
@ -31,6 +31,44 @@ the core size contracts and the actual example declarations, known arithmetic
results, painted sizing differences, complete rendering, and retained viewport updates. `make check` also results, painted sizing differences, complete rendering, and retained viewport updates. `make check` also
includes the example's regression tests. 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.
- `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 ## ORBIT spatial desktop and layer lab
`examples/layer-minimal.ebox` is the smallest static overlap example. The complete `examples/layer-minimal.ebox` is the smallest static overlap example. The complete

View File

@ -24,6 +24,33 @@ ECSS 0.1.0 与 TP 1.0.1 是互相独立的包,安装顺序任意;两者都
运行 `make size-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`,可检查核心尺寸契约、 运行 `make size-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs`,可检查核心尺寸契约、
示例的实际属性覆盖、已知计算结果、背景所指示的尺寸差异、完整渲染及保留状态的视口更新。`make check` 也包含此示例的回归测试。 示例的实际属性覆盖、已知计算结果、背景所指示的尺寸差异、完整渲染及保留状态的视口更新。`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 结束拖拽,
保留最后成功发布的位置。每次打开的预览状态互相独立。
横向按像素移动,纵向遵循 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` 的入口: 使用共享 `scripts/ebox-playground-flex-resize-evaluator.el` 的入口:

View File

@ -0,0 +1,178 @@
;; Layout and initial geometry live here; native gestures live in the companion.
(let* ((studio (desktop-demo-window-create
:id "desk-studio" :label "STUDIO" :accent "#BBA9FF" :ink "#241B40"
:left '(% 5) :top '(lh 1)))
(notes (desktop-demo-window-create
:id "desk-notes" :label "NOTES" :accent "#F6C879" :ink "#3B2917"
:left '(% 45) :top '(% 4)))
(signal (desktop-demo-window-create
:id "desk-signal" :label "SIGNAL" :accent "#8DE1E4" :ink "#15383D"
:left '(% 57) :top '(% 32)))
(terminal (desktop-demo-window-create
:id "desk-terminal" :label "TERMINAL" :accent "#A7E8BB" :ink "#1B3826"
:left '(% 8) :top '(% 46)))
(palette (desktop-demo-window-create
:id "desk-palette" :label "PALETTE" :accent "#F3A8C9" :ink "#402438"
:left '(% 43) :top '(% 65)))
(panes (list studio notes signal terminal palette))
(demo (desktop-demo-create :windows panes
:order (mapcar #'desktop-demo-window-id panes)
:active "desk-palette")))
`(column :width (vw 100) :height (vh 100) :overflow hidden
:background-color "#0B1020" :color "#CFD8EA"
(box :height (lh 3) :padding-inline (ch 1)
(row :height (lh 1)
(box :color "#BBA9FF" :font-weight bold "P A R A L L A X")
(box :padding-left (ch 2) :color "#657899" "DESKTOP / 05"))
(box :color "#F1F5FF" "A little space. A lot of possibilities.")
(box :color "#8F9FBD" "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) :color "#D7E3F8" :background-color "#25324B"
:pointer hand :hover-style (:background-color "#354662")
:help-echo "Restore the five original positions and stacking order."
:keymap ,(desktop-demo-control demo 'reset) "RESET")
(box :padding-inline (ch 1) :color "#D7E3F8" :background-color "#25324B"
:pointer hand :hover-style (:background-color "#354662")
:help-echo "Bring the backmost window to the front."
:keymap ,(desktop-demo-control demo 'next) "CYCLE")
(box :color "#657899" "FIVE IDEAS / ONE SHARED SPACE"))
(box :id "desktop-canvas" :height (calc (- (vh 100) (lh 7)))
:overflow hidden :background-color "#111A2C"
(box :position absolute :left (ch 1) :top (lh 0)
:color "#3F5273" "WORKSPACE 01 + + + + +")
(box :position absolute :left (% 68) :top (% 78)
:width (% 30) :color "#334866"
"+ . + . +\n. + . + .\n+ . + . +\n. + . + .")
(box :position absolute :left (ch 2) :top (% 93)
:color "#5A6D8E" "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
:background-color "#26223B" :color "#D6CDEF" :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)
:background-color "#25324B" :color "#C1CBE0" :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 :color "#F5F0FF" "Make space for the unexpected.")
(box :padding-top (lh 1) :color "#A999C8" "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) :background-color "#7561AC"
:color "#EAE1FF" :text-align center "\nFORM / 01")
(box :position absolute :left (ch 9) :top (lh 0)
:width (ch 13) :height (lh 3) :z-index 1
:background-color "#BBA9FF" :color "#302449"
:text-align center "\nSPACE / 02")
(box :position absolute :left (ch 18) :top (lh 2)
:width (ch 12) :height (lh 3) :z-index 2
:background-color "#D8EAAB" :color "#303D22"
:text-align center "\nPLAY / 03"))
(box :color "#B8A9D0" "Fragments become a composition.")
(box :padding-top (lh 1) :color "#85799F" "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
:background-color "#EEE3CB" :color "#514430" :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)
:background-color "#25324B" :color "#C1CBE0" :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) :color "#8C744E" "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) :color "#887651" "Less noise. More intention.")
(box :color "#A18B61" "------------------------------")
(box :color "#746345" "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
:background-color "#17343E" :color "#BFE5E9" :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)
:background-color "#25324B" :color "#C1CBE0" :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 :color "#E4FBF9" "AFTER HOURS")
(box :color "#7BAEB9" "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) :background-color "#518F9D")
(box :width (ch 2) :height (lh 2) :background-color "#8DE1E4")
(box :width (ch 2) :height (lh 1) :background-color "#518F9D")
(box :width (ch 2) :height (lh 2) :background-color "#BBA9FF")
(box :width (ch 2) :height (lh 1) :background-color "#8DE1E4")
(box :width (ch 2) :height (lh 2) :background-color "#BBA9FF")
(box :width (ch 2) :height (lh 1) :background-color "#518F9D"))
(box :color "#8DE1E4" "========------------ 03:42")
(box :padding-top (lh 1) :color "#699CA9" "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
:background-color "#101F22" :color "#B6D7CC" :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)
:background-color "#25324B" :color "#C1CBE0" :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 :color "#A7E8BB" "$ build something good")
(box :padding-top (lh 1) :color "#608C7F" " 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) :color "#A7E8BB" " 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
:background-color "#39283D" :color "#EBCBDE" :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)
:background-color "#F3A8C9" :color "#402438" :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) :color "#AF8DA9" "FIVE TONES / ONE ATMOSPHERE")
(flex :height (lh 2) :padding-top (lh 1) :gap ((lh 0) (ch 1))
(box :width (ch 4) :height (lh 1) :background-color "#BBA9FF")
(box :width (ch 4) :height (lh 1) :background-color "#F6C879")
(box :width (ch 4) :height (lh 1) :background-color "#8DE1E4")
(box :width (ch 4) :height (lh 1) :background-color "#A7E8BB")
(box :width (ch 4) :height (lh 1) :background-color "#F3A8C9"))
(box :padding-top (lh 1) :color "#F3A8C9" "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)
:background-color ,(if active (desktop-demo-window-accent pane) "#25324B")
:color ,(if active (desktop-demo-window-ink pane) "#C1CBE0")
: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) :color "#8F9FBD"
,(desktop-demo-status demo))))

View File

@ -0,0 +1,242 @@
;;; 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)
(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 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 (serial 5) dragging)
(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 (pane active)
"Return title and dock updates for PANE, highlighted when ACTIVE."
(let ((background (if active (desktop-demo-window-accent pane) "#25324B"))
(foreground (if active (desktop-demo-window-ink pane) "#C1CBE0")))
(mapcar (lambda (suffix)
(list (concat (desktop-demo-window-id pane) suffix)
:background-color background :color foreground))
'("-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-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
(desktop-demo--window demo (desktop-demo-active demo)) nil)
(desktop-demo--chrome 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
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-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))))
(_ (error "Unknown desktop action: %s" action))))))
(provide 'desktop-demo-reference)
;;; desktop-reference.el ends here

View File

@ -0,0 +1,410 @@
;;; 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)
(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-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