diff --git a/AGENTS.md b/AGENTS.md index 55e91a9..1bd3d3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/Makefile b/Makefile index 63e6a76..cce0050 100644 --- a/Makefile +++ b/Makefile @@ -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)" diff --git a/README.md b/README.md index 8474aa0..68084e0 100644 --- a/README.md +++ b/README.md @@ -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. + ## Native interaction lab diff --git a/examples/layer-minimal.ebox b/examples/layer-minimal.ebox new file mode 100644 index 0000000..ec72d29 --- /dev/null +++ b/examples/layer-minimal.ebox @@ -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")) diff --git a/examples/layer-reference.ebox b/examples/layer-reference.ebox new file mode 100644 index 0000000..6d26052 --- /dev/null +++ b/examples/layer-reference.ebox @@ -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."))) diff --git a/examples/layer-reference.el b/examples/layer-reference.el new file mode 100644 index 0000000..054585f --- /dev/null +++ b/examples/layer-reference.el @@ -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 diff --git a/tests/ebox-playground-layer-tests.el b/tests/ebox-playground-layer-tests.el new file mode 100644 index 0000000..9bd5931 --- /dev/null +++ b/tests/ebox-playground-layer-tests.el @@ -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