fix: keep workbench card heights independent when toggling help

This commit is contained in:
Kinneyzhang 2026-09-08 21:29:36 +08:00
parent 920efc36f7
commit ffafd668ab
4 changed files with 190 additions and 14 deletions

View File

@ -3,7 +3,7 @@
## Source of truth
- Status: Active
- Last refreshed: 2026-09-05
- Last refreshed: 2026-09-08
- Primary product surface: generic `etaf-playground.el` workspace plus the
`examples/research-shelf.etaf` / `.el` / `.ecss` consumer triplet
- Additional product surface: `examples/task-workbench.el`, a complete
@ -253,15 +253,22 @@ lane passed.
- Responsive: retain declarative Flex wrapping in source order. Side columns
have stable basis sizes; the task list grows. Root padding and viewport scroll
are legitimate composition and must work together without example workarounds.
- Panel alignment: each workspace Flex line stretches its columns to a shared
height, keeping the filter panel's bottom aligned with the main content. A
wrapped row sizes independently; no viewport-specific height constants.
- Panel alignment: workspace panels align at the top and keep their own
content-driven heights in layout lines. Toggling “显示说明” collapses the
detail content without changing the filter, task or service line counts. Following cards
and the footer may move naturally in document flow; do not reserve invisible
help space or introduce viewport-specific height constants. This follows
the user's 2026-09-08 review of disclosure-induced panel resizing.
- Native text geometry: Emacs shares ascent and descent across each displayed
row. Moving content may therefore change a card's pixel height while its
layout line count stays fixed. The user explicitly accepts this behavior;
preserve layout semantics and interaction instead of forcing uniform rows.
- Width distribution: filter/list/inspector grow with weights `1/4/1`, keeping
emphasis on the task list while a side column fills its line when wrapped.
Filter buttons wrap into a horizontal group when their panel has room.
Detail and service cards share a wrapping Flex: stacked in the desktop side
column, side by side with equal heights on a wider wrapped row, then stacked
again when each card's usable minimum width no longer fits.
column, side by side with independent heights on a wider wrapped row, then
stacked again when each card's usable minimum width no longer fits.
- Table sizing: the task title receives the remaining column width through the
existing `(fr 1)` Grid track convention; the action column keeps its compact
character width. Header and body consume the same tracks. Resizing changes

View File

@ -129,7 +129,7 @@
:align-items 'center
(slot :name 'toolbar)))
(flex :width 'stretch :flex-wrap 'wrap :gap '(1 (16))
:align-items 'stretch
:align-items 'flex-start
(slot))
(slot :name 'footer (text "准备就绪")))
:styles
@ -338,7 +338,7 @@
(etaf-data-load data))))
(flex :flex-wrap 'wrap :flex-basis 36 :flex-grow 1 :min-width 0
:align-content 'space-between :align-items 'stretch :gap '(1 (16))
:align-content 'space-between :align-items 'flex-start :gap '(1 (16))
(wb-detail :flex-basis 28 :flex-grow 1)
(wb-service :flex-basis 28 :flex-grow 1))

View File

@ -195,6 +195,49 @@ cannot establish that Emacs painted a control on the same visual line."
(list identity (car bounds) (cdr bounds))))
(number-sequence 1 10)))))
(defun wb-gui--panel-heights (context)
"Observe CONTEXT's four card identities and layout lines in source order."
(let ((runtime (wb-gui--runtime context))
panels)
(with-current-buffer (etaf-gui-verifier-context-target-buffer context)
(dolist (entry (etaf-runtime-host-props-entries runtime))
(let ((class (plist-get (cdr entry) :class)))
(when (member "etaf-panel" (if (stringp class) (split-string class) class))
(let* ((bounds (etaf-host-ref-bounds runtime (car entry)))
(start (car bounds))
(end (and bounds (1- (cdr bounds)))))
(unless (and start end)
(error "Workbench panel has no live bounds"))
(push (list :ref (car entry) :start start
:lines (1+ (- (line-number-at-pos end)
(line-number-at-pos start))))
panels))))))
(unless (= (length panels) 4)
(error "Expected four Workbench panels, got %d" (length panels)))
(sort panels (lambda (left right)
(< (plist-get left :start) (plist-get right :start))))))
(defun wb-gui--help-layout-preserved-p (context expanded-p)
"Check CONTEXT's disclosure state and unaffected card heights in layout lines.
EXPANDED-P also requires the detail card to recover its original height.
Native Emacs glyph rows may have different pixel heights as content moves."
(let* ((before (etaf-gui-verifier-context-get context 'help-panels-before))
(after (wb-gui--panel-heights context))
(height (lambda (panels index)
(plist-get (nth index panels) :lines))))
(etaf-gui-verifier-context-put context 'help-panels-current after)
(and before
(eq expanded-p (and (wb-gui--text-p context "点击行:选择任务") t))
(equal (mapcar (lambda (panel) (plist-get panel :ref)) before)
(mapcar (lambda (panel) (plist-get panel :ref)) after))
(cl-every (lambda (index)
(= (funcall height before index)
(funcall height after index)))
'(0 1 3))
(if expanded-p
(= (funcall height before 2) (funcall height after 2))
(< (funcall height after 2) (funcall height before 2))))))
(defun wb-gui--capture-theme-baseline (context)
"Capture CONTEXT's initial geometry once it survives another paint turn."
(when (etaf-gui-verifier-context-get context 'light-layout)
@ -507,10 +550,15 @@ cannot establish that Emacs painted a control on the same visual line."
(lambda (c) (wb-gui--enter c "每页条数 10 ✎" "2"))
(lambda (c) (and (equal (wb-gui--ids c) '(2 3))
(wb-gui--text-p c "Page 1 / 50"))))
(wb-gui--action "hide-help" (lambda (c) (wb-gui--press c "显示说明"))
(lambda (c) (not (wb-gui--text-p c "点击行:选择任务"))))
(wb-gui--action
"hide-help"
(lambda (c)
(etaf-gui-verifier-context-put
c 'help-panels-before (wb-gui--panel-heights c))
(wb-gui--press c "显示说明"))
(lambda (c) (wb-gui--help-layout-preserved-p c nil)))
(wb-gui--action "show-help" (lambda (c) (wb-gui--press c "显示说明"))
(lambda (c) (wb-gui--text-p c "点击行:选择任务")))
(lambda (c) (wb-gui--help-layout-preserved-p c t)))
(wb-gui--action "resource-failure" (lambda (c) (wb-gui--press c "模拟失败"))
(lambda (c) (wb-gui--text-p c "模拟加载失败")))
(wb-gui--action "resource-retry" (lambda (c) (wb-gui--press c "刷新 / 重试"))

View File

@ -108,15 +108,50 @@
(line-number-at-pos
(etaf-host-ref-position runtime (wb-test--control runtime "新增任务")))))))
(cl-defmacro wb-test--with-app ((runtime data) &rest body)
"Mount a fresh app bound as RUNTIME and DATA, then execute BODY."
(defun wb-test--card-layout (runtime control-label)
"Return public bounds and line geometry for CONTROL-LABEL's card in RUNTIME."
(let* ((control (wb-test--control runtime control-label))
(panels
(cl-remove-if-not
(lambda (entry)
(let ((classes (plist-get (cdr entry) :class)))
(member "etaf-panel"
(if (stringp classes) (split-string classes) classes))))
(etaf-runtime-host-props-entries runtime)))
(ancestries (etaf-runtime-host-ancestries
runtime (cons control (mapcar #'car panels))))
(matches
(cl-remove-if-not
(lambda (entry)
(memq (car (gethash (car entry) ancestries))
(gethash control ancestries)))
panels)))
(should (= (length matches) 1))
(let* ((host (caar matches))
(bounds (etaf-host-ref-bounds runtime host)))
(should bounds)
(should (< (car bounds) (cdr bounds)))
(with-current-buffer (etaf-runtime-buffer runtime)
(let ((top (line-number-at-pos (car bounds)))
(bottom (line-number-at-pos (1- (cdr bounds)))))
(list :host host :bounds (copy-tree bounds)
:top top :bottom bottom :height (1+ (- bottom top))
:left
(save-excursion
(goto-char (car bounds))
(ebox-string-pixel-width
(buffer-substring (line-beginning-position) (point))))))))))
(cl-defmacro wb-test--with-app ((runtime data &optional viewport-width) &rest body)
"Mount RUNTIME and DATA at optional VIEWPORT-WIDTH, then execute BODY."
(declare (indent 1))
`(with-temp-buffer
(let (,runtime ,data)
(unwind-protect
(progn
(etaf-mount (current-buffer) (etaf-view (wb-app))
'(:viewport-width 1600 :viewport-height 80))
(list :viewport-width (or ,viewport-width 1600)
:viewport-height 80))
(setq ,runtime (etaf-runtime-for-buffer (current-buffer))
,data (wb-test--data ,runtime))
,@body)
@ -288,6 +323,92 @@
(should (equal widths (wb-test--line-widths runtime)))
(should (wb-test--desktop-header-inline-p runtime)))))
(ert-deftest wb-workbench-explanation-toggle-isolates-card-heights ()
"Explanation changes only detail height across three rendered layouts."
(dolist (viewport '((250 . stacked-sidebar) (500 . side-by-side-sidebar)
(70 . single-column)))
(ert-info ((format "Workbench explanation layout: %S" viewport))
(wb-test--with-app (runtime data (car viewport))
(etaf-dispatch-event runtime (wb-test--row runtime 1) 'press)
(cl-labels
((cards ()
(mapcar (lambda (entry)
(cons (car entry) (wb-test--card-layout runtime (cdr entry))))
'((filter . "全部") (tasks . "每页条数 10 ✎")
(detail . "显示说明") (service . "刷新 / 重试")))))
(let* ((before (cards))
(text (wb-test--text runtime))
(items (etaf-value (etaf-data-items data)))
(selected (etaf-data-selected-item data))
(detail (wb-test--instance runtime 'wb-detail))
(service (wb-test--instance runtime 'wb-service))
(expanded (plist-get (etaf--component-instance-state detail) :expanded))
(resource (plist-get (etaf--component-instance-state service) :resource))
(rows (mapcar (lambda (id) (wb-test--row runtime id))
(number-sequence 1 10)))
(control (wb-test--control runtime "显示说明"))
(filter (alist-get 'filter before))
(tasks (alist-get 'tasks before))
(detail-layout (alist-get 'detail before))
(service-layout (alist-get 'service before)))
;; Prove the viewport actually exercises the intended wrapping
;; shape from published host bounds, rather than style declarations.
(pcase (cdr viewport)
('stacked-sidebar
(should (= (plist-get filter :top) (plist-get tasks :top)
(plist-get detail-layout :top)))
(should (< (plist-get tasks :left) (plist-get detail-layout :left)))
(should (< (plist-get detail-layout :bottom)
(plist-get service-layout :top))))
('side-by-side-sidebar
(should (= (plist-get filter :top) (plist-get tasks :top)
(plist-get detail-layout :top) (plist-get service-layout :top)))
(should (< (plist-get detail-layout :left)
(plist-get service-layout :left))))
('single-column
(should (< (plist-get filter :bottom) (plist-get tasks :top)))
(should (< (plist-get tasks :bottom) (plist-get detail-layout :top)))
(should (< (plist-get detail-layout :bottom)
(plist-get service-layout :top)))))
(should (etaf-value expanded))
(should (= (wb-task-id selected) 1))
(should (string-match-p "点击行:选择任务" text))
(dolist (visible '(nil t))
(wb-test--press runtime "显示说明")
(let ((current (cards)))
(message "Workbench explanation width=%d visible=%S heights=%S -> %S"
(car viewport) visible
(mapcar (lambda (entry) (plist-get (cdr entry) :height)) before)
(mapcar (lambda (entry) (plist-get (cdr entry) :height)) current))
(dolist (card '(filter tasks service))
(should (= (plist-get (alist-get card before) :height)
(plist-get (alist-get card current) :height))))
(should (funcall (if visible #'= #'<)
(plist-get (alist-get 'detail current) :height)
(plist-get detail-layout :height)))
(dolist (card '(filter tasks detail service))
(should (equal (plist-get (alist-get card before) :host)
(plist-get (alist-get card current) :host))))
(should (eq visible (etaf-value expanded)))
(should (eq detail (wb-test--instance runtime 'wb-detail)))
(should (eq service (wb-test--instance runtime 'wb-service)))
(should (eq items (etaf-value (etaf-data-items data))))
(should (eq selected (etaf-data-selected-item data)))
(should (= (etaf-value (etaf-data-total data)) 100))
(should (= (etaf-value (etaf-data-page data)) 1))
(should (= (etaf-value (etaf-data-page-size data)) 10))
(should (eq (etaf-resource-status resource) 'success))
(should (equal rows (mapcar (lambda (id) (wb-test--row runtime id))
(number-sequence 1 10))))
(should (equal control (wb-test--control runtime "显示说明")))
(should (string-match-p "设计组件接口" (wb-test--text runtime)))
(should (string-match-p "服务正常" (wb-test--text runtime)))
(should (eq visible
(not (null (string-match-p "点击行:选择任务"
(wb-test--text runtime))))))))
(should (equal before (cards)))
(should (equal text (wb-test--text runtime)))))))))
(ert-deftest wb-workbench-theme-render-failure-retains-published-handlers ()
"A failed theme render keeps the published UI and usable old callbacks."
(wb-test--with-app (runtime data)