Add layer examples and tests, document usage in README

This commit is contained in:
Kinneyzhang 2026-09-10 01:58:29 +08:00
parent 6cd5a67a35
commit 834c05f18b
7 changed files with 541 additions and 3 deletions

View File

@ -21,3 +21,5 @@ For size, unit, keyword, or sizing-function demonstration work, maintain `exampl
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.

View File

@ -7,7 +7,7 @@ EBOX_NATIVE_RELEASE_DIR = $(shell $(EMACS) -Q --batch $(LOAD_PATH) \
-l ../ebox/ebox-native-reflow.el \
--eval '(princ (expand-file-name (format "../ebox/native/target/%s/release/" (ebox-native-reflow--rust-target))))')
.PHONY: all compile test size-tests interaction-tests check checkdoc load performance performance-prepare \
.PHONY: all compile test size-tests interaction-tests layer-tests check checkdoc load performance performance-prepare \
performance-evaluator clean
all: check
@ -25,7 +25,14 @@ test: compile
$(EMACS) -Q --batch $(LOAD_PATH) \
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
-l tests/ebox-playground-tests.el -l tests/ebox-playground-size-tests.el \
-l tests/ebox-playground-interaction-tests.el -f ert-run-tests-batch-and-exit
-l tests/ebox-playground-interaction-tests.el -l tests/ebox-playground-layer-tests.el \
-f ert-run-tests-batch-and-exit
layer-tests: compile
$(MAKE) -C ../ebox layer-tests EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"
$(EMACS) -Q --batch $(LOAD_PATH) \
--eval '(setq load-prefer-newer t native-comp-jit-compilation nil)' \
-l tests/ebox-playground-layer-tests.el -f ert-run-tests-batch-and-exit
interaction-tests: compile
$(MAKE) -C ../ebox interaction-tests EMACS="$(EMACS)" ECSS_DIR="$(ECSS_DIR)" TP_DIR="$(TP_DIR)"

View File

@ -1,6 +1,6 @@
# 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.
`ebox-playground` is the independent, generic file runner for Ebox DSL examples. It uses only public Ebox APIs; concrete layouts live in readable `.ebox` fixtures under `examples/`. `flex-reference.ebox`, `grid-reference.ebox`, and `size-reference.ebox` demonstrate layout and sizing with a restrained terracotta, sage, blue, violet, ochre, and teal visual language. `interaction-reference.ebox` demonstrates native Ebox interaction and retained updates. `layer-reference.ebox` combines retained overlapping panels with a native anchored menu.
ECSS 0.1.0 and TP 1.0.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,59 @@ 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.
## Layer lab
`examples/layer-minimal.ebox` is the smallest static overlap example. The complete
lab 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.
- **UPDATE COVERED NOTE** changes a note fully covered by Panel A. Update it
twice, then **TOGGLE PANEL A** to reveal revision 02. The revealed note has
its own native action and live help; covering it never removes its logical
data or bindings.
- **MOVE PANEL A** changes `:left` and `:top` without adding flow rows.
**RAISE / LOWER A** changes `:z-index` between 2 and 4 while Panel B stays at
3. Opaque panel content and padding replace the lower layer's visible paint.
- The relative marker reserves its original flow row and paints one row lower.
Each parent forms a local stacking group automatically; no isolation
declaration is needed.
- **OPEN / CLOSE ROOT MENU** shows a menu whose logical owner clips to one
row. The nested absolute menu declares `:layer root` and anchors to the
trigger's semantic ID, so it can paint beyond that local clip.
**CYCLE PLACEMENT** visits `bottom-start`, `bottom-end`, `top-start`, and
`top-end`; **MOVE ANCHOR** moves the trigger and its open menu together.
Choosing sage or amber updates the result and closes the menu.
`layer-reference.ebox` contains layout data and state references;
`layer-reference.el` owns state and business actions. It uses only public
`ebox-keymap-create`, `ebox-help-create`, and `ebox-region-update` APIs.
Offsets use inline `ch` and block `lh` units. The text renderer composes whole
rows, and positioned content must share its host's line height.
Run the batch checks without opening GUI windows:
```sh
make layer-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
```
This target runs core layer contracts and the actual demo's native commands,
covered updates, movement, stacking, all four anchor placements, cross-buffer
mouse dispatch, and independent previews. After each command, the tests compare
the mounted output with a fresh render of the committed snapshot, including
paint and native capabilities. `make check` includes the Playground tests.
For repeated command timing, use the existing interaction evaluator with
`"examples/layer-reference.ebox"` and a toolbar label such as
`"UPDATE COVERED NOTE"`; see the invocation below.
<a id="native-interaction-lab"></a>
## Native interaction lab

View 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"))

View File

@ -0,0 +1,116 @@
;; 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 "#F4F3ED" :color "#273B42"
(box :padding ((lh 1) (ch 2)) :background-color "#153F4A" :color "#FFFFFF"
"EBOX / LAYER LAB\nRetained content, opaque overlap, native interaction.")
(box :padding-block (lh 1)
"Click a control, or move point into it and press RET / SPC.\n"
"All text shares one font size. Scroll with the wheel or C-v when needed.")
(box "01 / LAYERED DESK absolute position + local stacking")
(flex :flex-wrap wrap :gap ((lh 1) (ch 1)) :padding-block (lh 1)
:pointer hand :hover-style (:background-color "#CCE0D6")
(box :padding-inline (ch 1) :background-color "#DDE9DF"
: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 "#DDE9DF"
: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 "#DDE9DF"
: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 "#DDE9DF"
: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 "#DFE5E4"
(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 "#E8E1EE"
(box :id "relative-marker" :position relative :left (ch 4) :top (lh 1)
:width (ch 24) :height (lh 1) :z-index 1
:background-color "#765D87" :color "#FFFFFF" "RELATIVE / +4ch, +1lh")
(box "flow row 2: the shifted marker covers part of this row")
(box :id "relative-following" "FLOW ROW 3 / the original slot was kept")
(box "Each parent keeps a local stacking group automatically."))
(box :height (lh 1))
(box "03 / ANCHORED MENU nested owner + root projection")
(box :padding-block (lh 1)
"The owner below clips to one row. Its menu stays a logical child\n"
"but paints in the root layer, outside that local clip.")
(flex :gap ((lh 0) (ch 1)) :padding-bottom (lh 1) :pointer hand
:hover-style (:background-color "#CCE0D6")
(box :padding-inline (ch 1) :background-color "#DDE9DF"
: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 "#DDE9DF"
: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 "#E5EBE7"
"SPACE ABOVE / top placements can cover this area\n"
"........................................................................\n"
"........................................................................")
(box :id "menu-owner" :height (lh 1) :overflow hidden :background-color "#CFDCD5"
(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 a color")
(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 "#E5EBE7"
"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.")))

View File

@ -0,0 +1,98 @@
;;; 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"))
(defconst layer-demo-placements
[bottom-start bottom-end top-start top-end]
"Anchor placements visited by the lab's placement control.")
(defun layer-demo-lower-content (demo)
"Return the covered note's current content for DEMO."
(format "LOWER REV %02d\nUpdated even while covered.\nReveal me, then click this surface.\nLower activations: %02d\nThe logical note never left the tree."
(layer-demo-revision demo) (layer-demo-lower-clicks demo)))
(defun layer-demo-status (demo)
"Describe DEMO's currently published panel and menu state."
(format "A: %s / %s / z=%d | lower rev=%02d | menu: %s / %s | choice: %s"
(if (layer-demo-visible demo) "shown" "hidden")
(if (layer-demo-moved demo) "moved" "home")
(if (layer-demo-raised demo) 4 2)
(layer-demo-revision demo)
(if (layer-demo-menu-visible demo) "open" "closed")
(aref layer-demo-placements (layer-demo-placement demo))
(layer-demo-selection demo)))
(defun layer-demo-help (demo subject)
"Return live native help for DEMO's SUBJECT."
(pcase subject
('lower (format "Lower note revision %d; activated %d times. RET / SPC / click."
(layer-demo-revision demo) (layer-demo-lower-clicks demo)))
('menu (format "Menu is %s; placement %s; selected %s. RET / SPC / click."
(if (layer-demo-menu-visible demo) "open" "closed")
(aref layer-demo-placements (layer-demo-placement demo))
(layer-demo-selection demo)))
(_ (layer-demo-status demo))))
(defun layer-demo-action (demo action)
"Create a native keymap for DEMO's business ACTION."
(ebox-keymap-create
:activate
(lambda ()
(pcase action
('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)))
(ebox-region-update "layer-status" :content (layer-demo-status demo)))))
(provide 'layer-demo-reference)
;;; layer-reference.el ends here

View File

@ -0,0 +1,253 @@
;;; 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* ((snapshot (ebox-surface-buffer-snapshot (current-buffer)))
(fresh (ebox-render (plist-get snapshot :input))))
(should (ebox-canonical-input-p (plist-get snapshot :input)))
(should
(equal-including-properties
(ebox-playground-scroll-evaluator--visual-text (buffer-string))
(ebox-playground-scroll-evaluator--visual-text fresh)))))
(defun ebox-playground-layer-test--activate (label &optional key)
"Invoke the native command at LABEL using KEY, then verify its publication."
(let* ((keys (or key (kbd "RET")))
(position (ebox-playground-layer-test--position label))
(map (get-text-property position 'keymap)))
(goto-char position)
(should (keymapp map))
(let ((command (key-binding keys))
(last-input-event (aref (vconcat keys) (1- (length keys)))))
(should (commandp command))
(should (eq command (lookup-key map keys)))
(call-interactively command)
(ebox-playground-layer-test--parity)
command)))
(defun ebox-playground-layer-test--click (label)
"Dispatch LABEL's native mouse command while another buffer is current."
(let* ((buffer (current-buffer))
(position (ebox-playground-layer-test--position label))
(map (get-text-property position 'keymap)))
(save-window-excursion
(set-window-buffer (selected-window) buffer)
(let* ((event (list 'mouse-1 (list (selected-window) position '(0 . 0) 0)))
(last-input-event event)
(command (key-binding [mouse-1] nil nil position)))
(should (commandp command))
(should (eq command (lookup-key map [mouse-1])))
(with-temp-buffer
(call-interactively command nil (vector event))
(should (zerop (buffer-size))))))
(ebox-playground-layer-test--parity)))
(defun ebox-playground-layer-test--nodes (form)
"Return DSL nodes in FORM without walking opaque property values."
(when (and (consp form) (memq (car form) '(box text row column flex grid)))
(let ((tail (cdr form)) (nodes (list form)))
(while tail
(if (keywordp (car tail))
(setq tail (cddr tail))
(setq nodes (append nodes (ebox-playground-layer-test--nodes (car tail)))
tail (cdr tail))))
nodes)))
(ert-deftest ebox-playground-layer-reference-declares-layers-with-one-font-size ()
"Actual layout data contains the taught contracts and no font-size override."
(let* ((form (ebox-playground--evaluate-form
(ebox-playground--read-file ebox-playground-layer-test--file)
ebox-playground-layer-test--file))
(nodes (ebox-playground-layer-test--nodes form))
(owner (cl-find-if
(lambda (node) (equal (plist-get (cdr node) :id) "menu-owner"))
nodes))
(menu (cl-find-if
(lambda (node) (equal (plist-get (cdr node) :id) "root-menu"))
(ebox-playground-layer-test--nodes owner))))
(should (ebox-canonical-input-p (ebox-build form)))
(should (eq (plist-get (cdr owner) :overflow) 'hidden))
(should (equal (plist-get (cdr owner) :height) '(lh 1)))
(should (eq (plist-get (cdr menu) :layer) 'root))
(should (equal (plist-get (cdr menu) :anchor) "menu-trigger"))
(dolist (position '(relative absolute))
(should (cl-some (lambda (node)
(eq (plist-get (cdr node) :position) position)) nodes)))
(dolist (node nodes)
(should-not (plist-member (cdr node) :isolation))
(should-not (plist-member (cdr node) :font-size)))))
(ert-deftest ebox-playground-layer-covered-updates-reveal-latest-native-surface ()
"Covered text remains retained; revealing it restores current native help."
(ebox-playground-layer-test--with-preview (buffer)
(should-not (string-match-p "LOWER REV" (buffer-string)))
(should (ebox-region-resolve buffer "lower-note"))
(ebox-playground-layer-test--activate "UPDATE COVERED NOTE")
(ebox-playground-layer-test--activate "UPDATE COVERED NOTE" (kbd "SPC"))
(should-not (string-match-p "LOWER REV" (buffer-string)))
(should (string-match-p "lower rev=02" (buffer-string)))
(ebox-playground-layer-test--activate "TOGGLE PANEL A")
(let* ((position (ebox-playground-layer-test--position "LOWER REV 02"))
(help (get-text-property position 'help-echo)))
(should (eq (get-text-property position 'pointer) 'hand))
(should (get-text-property position 'mouse-face))
(should (functionp help))
(should (string-match-p "revision 2; activated 0"
(funcall help (selected-window) buffer position))))
(ebox-playground-layer-test--activate "LOWER REV 02" [return])
(let* ((position (ebox-playground-layer-test--position "LOWER REV 02"))
(help (get-text-property position 'help-echo)))
(should (string-match-p "revision 2; activated 1"
(funcall help (selected-window) buffer position))))
(ebox-playground-layer-test--activate "TOGGLE PANEL A")
(should-not (string-match-p "LOWER REV" (buffer-string)))
(ebox-playground-layer-test--activate "TOGGLE PANEL A")
(should (integerp (ebox-playground-layer-test--position "LOWER REV 02")))))
(ert-deftest ebox-playground-layer-move-and-raise-preserve-flow-and-coverage ()
"Moving changes paint coordinates; raising changes only overlap ownership."
(ebox-playground-layer-test--with-preview (buffer)
(let ((home (ebox-playground-layer-test--coordinates "PANEL A / blue"))
(desk-end (ebox-playground-layer-test--coordinates "DESK END")))
(should-not (string-match-p "A survives under the amber panel" (buffer-string)))
(ebox-playground-layer-test--activate "RAISE / LOWER A")
(should (integerp (ebox-playground-layer-test--position
"A survives under the amber panel")))
(should-not (string-match-p "PANEL B / amber" (buffer-string)))
(should (equal home (ebox-playground-layer-test--coordinates "PANEL A / blue")))
(ebox-playground-layer-test--activate "RAISE / LOWER A")
(should (integerp (ebox-playground-layer-test--position "PANEL B / amber")))
(ebox-playground-layer-test--activate "MOVE PANEL A")
(let ((moved (ebox-playground-layer-test--coordinates "PANEL A / blue")))
(should (> (car moved) (car home)))
(should (= (cdr moved) (- (cdr home) 2))))
(should (equal desk-end (ebox-playground-layer-test--coordinates "DESK END")))
(ebox-playground-layer-test--activate "MOVE PANEL A")
(should (equal home (ebox-playground-layer-test--coordinates "PANEL A / blue"))))))
(ert-deftest ebox-playground-layer-relative-marker-reserves-its-original-row ()
"The relative marker moves into row two while row three retains its slot."
(ebox-playground-layer-test--with-preview (buffer)
(let ((marker (ebox-playground-layer-test--coordinates "RELATIVE / +4ch, +1lh"))
(following (ebox-playground-layer-test--coordinates "FLOW ROW 3")))
(should (= (1+ (cdr marker)) (cdr following)))
(should (> (car marker) (car following)))
(save-excursion
(goto-char (ebox-playground-layer-test--position "RELATIVE / +4ch, +1lh"))
(forward-line -1)
(should (string-match-p
"\\`[[:space:]]*\\'"
(buffer-substring-no-properties
(line-beginning-position) (line-end-position)))))
(ebox-playground-layer-test--parity))))
(ert-deftest ebox-playground-layer-root-menu-follows-all-four-anchor-placements ()
"The nested menu escapes its one-row owner and follows the moved trigger."
(ebox-playground-layer-test--with-preview (buffer)
(should-not (string-match-p "ROOT MENU / choose" (buffer-string)))
(ebox-playground-layer-test--activate "OPEN / CLOSE ROOT MENU")
(let* ((trigger (ebox-playground-layer-test--coordinates "OPEN / CLOSE ROOT MENU"))
(bottom-start (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
(should (= (cdr bottom-start) (1+ (cdr trigger))))
(should (= (car bottom-start) (car trigger)))
(ebox-playground-layer-test--activate "CYCLE PLACEMENT")
(let ((bottom-end (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
(should (= (cdr bottom-end) (cdr bottom-start)))
(should (> (car bottom-end) (car bottom-start)))
(ebox-playground-layer-test--activate "CYCLE PLACEMENT")
(let ((top-start (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
(should (= (cdr top-start) (- (cdr trigger) 3)))
(should (= (car top-start) (car bottom-start))))
(ebox-playground-layer-test--activate "CYCLE PLACEMENT")
(let ((top-end (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
(should (= (cdr top-end) (- (cdr trigger) 3)))
(should (= (car top-end) (car bottom-end))))
(ebox-playground-layer-test--activate "CYCLE PLACEMENT"))
(should (equal bottom-start (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
(ebox-playground-layer-test--activate "MOVE ANCHOR")
(let ((moved-trigger (ebox-playground-layer-test--coordinates "OPEN / CLOSE ROOT MENU"))
(moved-menu (ebox-playground-layer-test--coordinates "ROOT MENU / choose")))
(should (> (car moved-trigger) (car trigger)))
(should (= (- (car moved-menu) (car bottom-start))
(- (car moved-trigger) (car trigger))))))
(let ((position (ebox-playground-layer-test--position "CHOOSE SAGE")))
(dolist (property '(keymap mouse-face help-echo pointer))
(should (get-text-property position property))))
(ebox-playground-layer-test--click "CHOOSE SAGE")
(should-not (string-match-p "ROOT MENU / choose" (buffer-string)))
(should (integerp (ebox-playground-layer-test--position
"Selected sage from the anchored menu.")))
(ebox-playground-layer-test--activate "OPEN / CLOSE ROOT MENU")
(ebox-playground-layer-test--activate "CHOOSE AMBER" (kbd "SPC"))
(should (integerp (ebox-playground-layer-test--position
"Selected amber from the anchored menu.")))))
(ert-deftest ebox-playground-layer-previews-retain-independent-state-at-two-widths ()
"Separate previews keep independent notes and menus, with bounded rendering."
(ebox-playground-layer-test--with-preview (first)
(ebox-playground-layer-test--activate "UPDATE COVERED NOTE")
(ebox-playground-layer-test--with-preview (second 1000)
(should (string-match-p "lower rev=00" (buffer-string)))
(ebox-playground-layer-test--activate "OPEN / CLOSE ROOT MENU")
(with-current-buffer first
(should (string-match-p "lower rev=01" (buffer-string)))
(should-not (string-match-p "ROOT MENU / choose" (buffer-string)))
(ebox-playground-layer-test--parity))
(dolist (line (ebox-string-lines (buffer-string)))
(should (<= (ebox-string-pixel-width line) 1000)))
(ebox-playground-layer-test--parity))))
(provide 'ebox-playground-layer-tests)
;;; ebox-playground-layer-tests.el ends here