fix: keep workbench card heights independent when toggling help
This commit is contained in:
parent
920efc36f7
commit
ffafd668ab
19
DESIGN.md
19
DESIGN.md
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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 "刷新 / 重试"))
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user